Compare commits

...

196 Commits

Author SHA1 Message Date
Eric Allam 1d7e5737a0 Fix pnpm lock 2023-08-28 16:07:18 +01:00
github-actions[bot] 305e3b7ef2 chore: Update version for release (#417)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-28 16:06:35 +01:00
Eric Allam 03721cb18d Merge branch 'Chigala-refactor/lower-max-retries-for-specific-tasks' 2023-08-28 14:25:59 +01:00
Eric Allam 773a6e2c81 A couple of fixes 2023-08-28 14:25:41 +01:00
Eric Allam 2f13ac100f Merge branch 'refactor/lower-max-retries-for-specific-tasks' of https://github.com/Chigala/trigger.dev into Chigala-refactor/lower-max-retries-for-specific-tasks 2023-08-28 14:02:03 +01:00
Eric Allam a10782490f @trigger.dev/stripe: Added PaymentIntent and Payout trigger events 2023-08-28 12:40:02 +01:00
Eric Allam 6ad91123f2 Turned off automatic github releases 2023-08-28 10:49:24 +01:00
Eric Allam 81f2d5e4ec Fix pnpm lock 2023-08-28 10:45:16 +01:00
github-actions[bot] f249d8defa chore: Update version for release (#414)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-28 10:43:25 +01:00
Eric Allam 09aef5cda7 Merge branch 'main' of https://github.com/triggerdotdev/trigger.dev 2023-08-28 10:38:12 +01:00
Eric Allam 3028b6ad9d Try to automatically create github releases when packages are updated 2023-08-28 10:38:01 +01:00
Wesley 060b650845 fix/TRI 1149/cover all duplicated cases (#398)
* feat: select node by property key name

* feat: get `IfStatement`s recursively

* refactor: make `getNodeBody` more readable

* refactor: enshorter rule code

* refactor: remove additional spaces

* Create lovely-teachers-taste.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-28 10:26:31 +01:00
Chigala 6186a14398 fix: CLI init doesn't work outside of a GIT repo (#405) 2023-08-28 10:25:30 +01:00
Eric Allam 916a353660 bugfix: retrying tasks no longer incorrectly complete successfully 2023-08-28 10:16:47 +01:00
neo773 699878a5b1 feat: Add update checker for CLI (#412)
* feat: Add update checker for CLI

* Create soft-hounds-clap.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-28 09:36:53 +01:00
Chigala d2c9b64212 fix: CLI dev command doesn't work if you're already running ngrok (#407)
* fix: CLI dev command doesn't work if you're already running ngrok

* added changeset
2023-08-28 09:26:26 +01:00
Eric Allam 3102deccfb Add onAccountUpdated to the list of stripe triggers in the docs 2023-08-27 21:54:22 +01:00
Eric Allam 97c1b51332 Fix pnpm lock 2023-08-27 14:13:08 +01:00
github-actions[bot] 4ab7082954 chore: Update version for release (#408)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-27 14:12:33 +01:00
Eric Allam 760f5de248 Adding additional stripe triggers and added initial stripe integration docs 2023-08-27 14:09:24 +01:00
nicktrn de7e8c783e docs: fix fly.io direct connection url (#397) 2023-08-25 10:22:56 +01:00
Sondreal 45b7af53e6 Update self-hosting.mdx (#395) 2023-08-25 08:42:08 +01:00
Chigala aa7458fe37 refactor: reduced max retries on some task to 1 on development 2023-08-24 23:50:33 +01:00
James Ritchie 1fd1d26780 Made jobs and runs into nouns 2023-08-24 15:36:14 +01:00
Eric Allam 6979eb6e66 Fixed pnpm lock 2023-08-24 15:27:08 +01:00
github-actions[bot] b0f926d28d chore: Update version for release (#375)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-24 15:26:42 +01:00
Eric Allam 82d8e7d3f3 Added the new tasks to the openai docs 2023-08-24 15:24:30 +01:00
James Ritchie a214411218 Changed the button text for Delete Job on the settings page
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-24 15:04:28 +01:00
James Ritchie 2806cee7de Conditionally show the disable Job text by the Delete button 2023-08-24 14:36:02 +01:00
James Ritchie bd28556bf3 Made the loading spinner white 2023-08-24 14:25:59 +01:00
James Ritchie 19a8d39fb0 The Delete button displays full width again 2023-08-24 14:11:55 +01:00
Chigala a907e2a988 chore: updated the type in the eventId argument (#389)
* chore: updated the type in the eventId argument

* Create nervous-masks-rest.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-24 11:51:06 +01:00
Eric Allam 403acac5ef #344 Upgrading the openai package to v4 (#383)
* #344 Upgrading the openai package to v4

* Create neat-roses-wait.md

* Fixed a couple of issues with the openai v4 upgrade

* Added fine tuning job tasks
2023-08-24 11:48:39 +01:00
James Ritchie 708ebbde14 Features: Disabling and deleting jobs (#381)
* Don’t show the Ready To Run Job prompt if you have an Integration that needs attention

* Don’t highlight the row red or green

* Improvements to the contrast of the app sections and dividers

* Removed the Jobs page title as it’s duped info

* using the new border variable

* Sticky last table cell

* Improved the sticky last table cell

* Make any last cell in a table sticky by adding isSticky to it

* Added a dropdown menu to the menu table cell

* table rows can be marked as disabled by adding disabled

* Using the jobTestPath function for the test path

* tidy up imports

* Removed un-used props

* Removed the green badge variant

* Added a new status badge to the Runs table

* Clicking the gradient clicks the row

* Delete Job triggers a modal popup

* Added a large danger button type

* Added some modal styling and started adding data

* Added more styling and data to the delete job modal

* A table can now be given a full width prop

* Large danger button added to Storybook

* Danger button disabled state looks disabled now

* New active badge component to display in the table and logic for showing the env data

* Style updates to the dialog component

* active and job status badges can now have a small size

* Added a new named icon

* The Job page shows the Job status in the PageInfoRow

* Small badge style update

* Runs table has a sticky right cell

* Created a JobStatusTable component

* Added some placeholder help panel content for disabling a Job

* WIP creating a Settings page

* Added a delete button that triggers the delete modal – just need data hooking up

* Implemented deleting jobs from the dashboard

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-24 10:30:00 +01:00
Eric Allam 3ce5397072 Implement the disable job feature backend when indexing endpoints (#382) 2023-08-23 10:30:06 +01:00
Liran Tal ee79ab30a9 feat: introduce support for native astro integration package (#354)
* feat: introduce support for native astro integration package

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* fix: update license copyright to match other packages from trigger

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* fix: doc update to refer to the official astro package from trigger

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* fix: repo URLs update accordingly to official repo

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* fix: remove myself as author of the package to not confuse people

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* Remove package-lock because this is now in the pnpm monorepo

---------

Signed-off-by: Liran Tal <liran.tal@gmail.com>
Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-23 10:29:10 +01:00
Eric Allam 2b21f5ba8a bugfix: specifiying an alternate WORKER_SCHEMA now works 2023-08-22 13:15:12 +01:00
Eric Allam 8c27236bc9 Merge branch 'rishi-raj-jain-main' 2023-08-22 11:51:02 +01:00
Eric Allam b8d191052b A couple tweaks to the update command 2023-08-22 11:50:52 +01:00
Eric Allam 979f5f88ae Merge branch 'main' of https://github.com/rishi-raj-jain/trigger.dev into rishi-raj-jain-main
# Conflicts:
#	pnpm-lock.yaml
2023-08-22 11:32:31 +01:00
Eric Allam fef1670533 Improved the testing CLI contributing guide 2023-08-22 11:28:03 +01:00
Eric Allam 3897e6e695 @trigger.dev/cli ini: Make it more clear which API key the init command expects 2023-08-22 11:16:29 +01:00
Eric Allam fc083de102 Merge branch 'Chigala-bugfix/cli-init' 2023-08-22 11:06:44 +01:00
Eric Allam c7c5fd7fac Fixed the examples job path 2023-08-22 11:06:32 +01:00
Eric Allam 4630960478 Merge branch 'bugfix/cli-init' of https://github.com/Chigala/trigger.dev into Chigala-bugfix/cli-init 2023-08-22 10:57:05 +01:00
Eric Allam 302bd02ff1 Issue #377: only expose the external eventId in the API (#380)
* Issue #377: only expose the external eventId in the API

* Create eighty-zebras-bow.md
2023-08-22 10:46:46 +01:00
Eric Allam b5db9f5e5c Added MIT license to all public packages 2023-08-21 22:38:11 +01:00
Rishi Raj Jain 4b64c2d020 remove Index type 2023-08-22 02:19:36 +05:30
Rishi Raj Jain 0c01e58db9 await 2023-08-22 02:15:43 +05:30
Rishi Raj Jain 367ce2e412 use exitsing methods 2023-08-22 02:07:14 +05:30
Rishi Raj Jain 92f80fd87c Merge branch 'triggerdotdev:main' into main 2023-08-22 02:03:43 +05:30
Chigala 135cb4921f added changeset 2023-08-21 19:02:52 +01:00
Chigala 6cbc4f1190 fix: CLI init now logs the correct app route path. 2023-08-21 18:59:31 +01:00
Rishi Raj Jain 1bf84548d7 add types 2023-08-21 23:19:03 +05:30
Rishi Raj Jain ce95744e98 use npm-check-updates 2023-08-21 23:11:15 +05:30
Eric Allam 0493cebaed Merge branch 'Chigala-feat/cancelEvent-function' 2023-08-21 17:49:48 +01:00
Eric Allam ac98219b76 adding changeset 2023-08-21 17:49:28 +01:00
Eric Allam c8ddc19d5a Fixed an issue with IngestSendEvent not returning an existing event 2023-08-21 17:48:34 +01:00
Eric Allam c6df574689 Fixed io.cancelEvent (shouldn’t be a noop) 2023-08-21 17:48:21 +01:00
Rishi Raj Jain 232b7cd26a Merge branch 'triggerdotdev:main' into main 2023-08-21 21:49:35 +05:30
Eric Allam b9dc7ce89e Merge branch 'feat/cancelEvent-function' of https://github.com/Chigala/trigger.dev into Chigala-feat/cancelEvent-function
# Conflicts:
#	apps/webapp/app/platform/zodWorker.server.ts
2023-08-21 16:57:36 +01:00
Chigala 8cf85443d5 bugfix: jsx extensions breaks cli init (#374)
* bugfix: jsx extensions breaks cli init

* Update init.ts (adding ts extension to check)

* Create rude-spoons-compete.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-21 16:03:10 +01:00
Hugo Nogueira dd10717628 feat: add hostname option to the dev command (#370)
* feat: add hostname option to the dev command

- This commit adds a `hostname` option to the cli `dev` command, to allow the cli to point to nextjs applications running on different hostnames other than `localhost`. Example: the nextjs app was started using a --hostname 0.0.0.0 option to be able to be visible from inside docker containers. So adding --hostname 0.0.0.0 to `trigger-cli dev` would make it work.

* docs: add dev cli command hostname option documentation

* feat: add hostname option to the dev command

- This commit adds a `hostname` option to the cli `dev` command, to allow the cli to point to nextjs applications running on different hostnames other than `localhost`. Example: the nextjs app was started using a --hostname 0.0.0.0 option to be able to be visible from inside docker containers. So adding --hostname 0.0.0.0 to `trigger-cli dev` would make it work.

* docs: add dev cli command hostname option documentation

* Update hip-coins-reply.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-21 15:46:16 +01:00
Eric Allam 8bd1ca8b66 Fix pnpm lock file 2023-08-21 14:01:38 +01:00
github-actions[bot] 7f5ce165a0 chore: Update version for release (#361)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-21 13:45:35 +01:00
Eric Allam 7df1b90928 Tweak some of the task max attempts
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-21 09:52:55 +01:00
Atharva Deshpande 57a7e00bfa Fix spelling mistake in README.md (#371) 2023-08-19 15:23:22 +01:00
Eric Allam 7362feed71 Fix for old clients missing the cached tasks 2023-08-19 07:32:34 +01:00
Eric Allam b1b9321ad2 Job run performance improvements and adding "worker only" mode (#360)
* WIP job run performance improvements

- Added a `perf` tool to better measure job run performance under heavy load
- Removed `runFinished` job (not really needed)
- startQueuedRuns now uses a jobKey with replace
- Fixed an issue with ZodWorker when using jobKey

* Publish improvement docker images

* fixed the improvement docker publishing

* Downgrade back to prisma 4.16.0 because 5.1.x broke docker builds

* Changes to how queued runs work

- Split the worker into two different workers, one dedicated to performRunExecution
- Schedule performRunExecution in a single place, with a queue and using a round robin manually controlled concurrency
- Remove startQueuedRuns
- All runs are queued before they are started
- Setting the worker maxPoolSize to the same as the worker concurrency
- Starting to be able to split the docker image

* Remove queue name from startRun graphile job

* Make the prisma connection pool stuff configurable through env vars

* Hardcode (for now) the max concurrent runs limit

* Rewrite performRunExecution to be more performant

PerformRunExecutionV2:

- Does not create and manage jobRunExecution records
- Does not reimplement retrying, uses graphile worker retrying instead

I’ve kept around PerformRunExecutionV1 so this works when deploying. Definitely needs LOTS of testing

* Fix issues with cached tasks

- Limit the size of the cached tasks sent when executing a run, using the knapsack problem dynamic programming approach
- Actually USE the cached tasks in IO by using the idempotencyKey instead of the task ID
- Remove output from all logs
- Added a stress test job catalog

* Forgot to commit the logger updates

* Never log connectionString

* Login to docker hub to get around rate limits

* Add additional logging to the graphile workers

* Fix the *_ENABLED  env vars

* Allow adding and removing jobs to be done from the webapp

* Don’t set the job to failed if it’s being retried

* Deprecated queue options in the job and removed startPosition. Now using the job/env combo as the job queue name

* Dequeung jobs doesn’t check if the runner is initialized

* Fixed issues with retrying a run getting stuck on a cancelled task, and errors from parsing the results of dequeing a job

* Remove queued round robin thing that isn’t used anymore

* Added slack to job catalog

* Better forwards compat

* Added long delay

* Fixed lock file
2023-08-18 15:25:26 +01:00
Dan a69f756e34 Added delay example to init.ts so it works with the pages router (#363) 2023-08-18 15:22:18 +01:00
Chigala 18a02829e3 set the jobkey as event:event.id 2023-08-18 15:21:09 +01:00
Matt Aitken 0f6e580641 Added SendGrid to the integrations table in docs 2023-08-18 15:13:16 +01:00
Chigala 796f1209f2 Docs/sendgrid doc (#359)
* added sengrid integration to the catalog

* added documention for sendgrid integration

* updated the mint.json file
2023-08-18 15:09:04 +01:00
Liran Tal 4ce96b7d28 fix: avoid code duplication by repostioning the default switch case (#356) 2023-08-18 14:48:07 +01:00
Wesley b86ffa0d3d test/add test on get user pkg manager (#349)
* chore: make possible to write tests aside with implementations

* test: write getUserPkgManager test TO-DOs

* test: add test to building the path

* test: add tests to manual file checking

* test: add tests to npm_config_user_agent

* Update packages/cli/src/utils/getUserPkgManager.spec.ts
2023-08-18 14:43:19 +01:00
Wesley 3ee7cd6ff6 feat: parse callback result with zod before return (#342) 2023-08-18 14:33:42 +01:00
Wesley 74686c00cb feature/TRI 712/create eslint plugin to ensure uniqueness on task keys (#324)
* test: add test to no-duplicated-task-keys rule

* feat: implement no-duplicated-task-keys

* chore: create testing scripts

* refactor: fill in rule meta

* feat: export no-duplicated-task-keys rule

* chore: bump package version to 0.0.1

* chore: create eslint plugin

* feat: create no-duplicated-task-keys rule

* refactor: delete no-duplicated-task-keys from config-custom

* refactor: rename eslint-plugin folder

* feat: cover case from nextjs-example

* feat: cover additional cases from examples/package-tester

* chore: add eslint-plugin on nextjs-example

* feat: add more cases from `nextjs-example`

* revert: rollback changes on eslint-config-custom

* Set version to 2.0.9, inline with other packages

* Create chilly-pianos-try.md

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-18 12:57:12 +01:00
Liran Tal 8f3e550d03 fix: small typo in function docblocks (#362) 2023-08-18 12:17:45 +01:00
Matt Aitken 65cff19ead Merge branch 'main' into main 2023-08-18 11:20:44 +01:00
Matt Aitken 591422b8cf CLI: added whoami command, fixed TypeScript error 2023-08-18 11:18:51 +01:00
neo773 bbaa6ba156 [TRI-1006] New @trigger.dev/cli whoami command. (#316)
* FIx: [TRI-1006] New @trigger.dev/cli whoami command.

* revert dev.ts zod schema to original

* Removed telemetry

* remove all telemetry

* Made the clientId optional again

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-18 10:41:06 +01:00
Liran Tal e20fa3c2ec fix: explicitly set a charset for rendered HTML documents (#355) 2023-08-18 06:49:39 +01:00
Liran Tal 57514f8771 fix: code quality nitpicks for some issues spotted (#357) 2023-08-18 06:48:48 +01:00
Chigala ef1a1ff343 added io.cancelEvent support and fixed other requested changes 2023-08-18 06:21:57 +01:00
Rishi Raj Jain 2fbaa30b9f Update index.ts 2023-08-17 23:45:44 +05:30
Rishi Raj Jain 1199e934c2 Merge branch 'main' into main 2023-08-17 23:45:12 +05:30
Wesley 83bcefe10c refactor: remove .js extension from cli package (#350)
* refactor: remove `.js` extension from cli package

* revert: rollback changes

* revert: rollback unnecessary changes

* Update createIntegration.ts

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-17 16:03:00 +01:00
Wesley 4ca9f182d7 test/TRI 887/setup jest on cli package (#326)
* chore: run `pnpm install --prefer-frozen-lockfile`

* chore: install `jest` and `ts-jest`

* test: create `jest.config.js`

* chore: install `@types/jest`

* chore: add `jest` on `tsconfig.json` `types` property

* chore: create test script

* test: create example test 🎉🎊🕺🏼

* chore: install `@gmrchk/cli-testing-library`

* chore: override `tsconfig` with `@trigger.dev/tsconfig`

* test: make poc of cli test

* chore: run build before test

* docs: improve comment

* Added a changeset

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-16 22:35:23 +01:00
Matt Aitken 5dec3ed4e6 Latest lockfile 2023-08-16 15:56:30 +01:00
github-actions[bot] 955961eb5f chore: Update version for release (#329)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-16 15:53:53 +01:00
Matt Aitken 33184a8130 Add subtasks to the schema/types when getting an individual run (#343)
* Add subtasks to the schema/types when getting an individual run

* Added a changeset
2023-08-16 15:49:08 +01:00
Eric Allam 5611768854 Improve the job reference 2023-08-15 10:20:30 +01:00
Chigala a3278cb8a2 bug fixes 2023-08-15 00:16:38 +01:00
Eric Allam d0e6442872 Add additional sendEvent exampels 2023-08-14 22:04:40 +01:00
Rishi Raj Jain 05b5ed6973 Update update.ts 2023-08-14 21:42:08 +05:30
Rishi Raj Jain 859425f093 Update update.ts 2023-08-14 21:32:55 +05:30
Matt Aitken 1e374d30d0 Latest lockfile… 2023-08-14 16:54:40 +01:00
Matt Aitken 9f649d2a27 Lock prisma to 5.1.1 2023-08-14 16:52:30 +01:00
Chigala 0a20b6e05a chore: updated prisma from version 4.16.0 to 5.1.0 (#254)
* chore: update prisma from version 4.16.0 to 5.1.0

* fixed the ts-errors due to the prisma update

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-14 16:36:40 +01:00
Eric Allam 78e2d0e9f9 Fix SSE causing app crashes when inner async loop takes too long
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-14 16:21:45 +01:00
Chigala 24c8f08267 feat: added the ability to cancel Events based on the event ID 2023-08-14 14:56:48 +01:00
Eric Allam 12de1b0d34 Update docs to reference @trigger.dev/react@latest instead of @next 2023-08-14 13:46:58 +01:00
Eric Allam b62df731fc Fixed the example Job trigger file import path when using Pages Router with no path alias 2023-08-14 13:40:59 +01:00
Liran Tal e347389b64 docs: Update integrations.mdx (#328)
* Update integrations.mdx

The first sentence in this paragraph for `### API Keys and Tokens` was missing a word or two and it wasn't entirely clear what did the original author of this docs page intended.

I've entered what seems to be the missing context but happy to re-phrase too if another wording makes better sense here.

* Made it clear that API keys are provided by you in your code

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-14 13:24:31 +01:00
Liran Tal 10f92a6124 docs: Update runs.mdx (#327)
* docs: Update runs.mdx

From what I can tell, it is expected for run functions to return with some sort of payload. Providing an example here as a reference and guideline.

* Made it clear returning data is optional and added some more detail below

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-14 13:22:14 +01:00
Eric Allam b957aa1752 Remove deprecated invitation-code route 2023-08-14 09:41:05 +01:00
Rishi Raj Jain 3159619852 revert pnpm changes 2023-08-12 11:28:27 +05:30
Rishi Raj Jain 300c102b31 remvoe axios and use node-fetch 2023-08-12 11:26:08 +05:30
Rishi Raj Jain 9e0b152c0e Update package.json 2023-08-12 11:19:18 +05:30
Arshad Jamal a998d6c505 Corrected clone link for Windows (#319) 2023-08-12 06:23:45 +01:00
Rishi Raj Jain 4e78da319c Create swift-eagles-float.md 2023-08-11 23:54:06 +05:30
Rishi Raj Jain 2c37091b08 complete the script 2023-08-11 23:48:37 +05:30
James Ritchie 5f739ee312 Removed ts-ignore and added types 2023-08-11 16:11:27 +01:00
James Ritchie fccba7db98 Upgraded the confetti to fireworks 2023-08-11 16:09:47 +01:00
Eric Allam 0f7b7ff6d6 Fix pnpm lock
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-11 15:52:52 +01:00
nicktrn 603ffbc426 docs: add supabase guide (#297)
* docs: add supabase guide

* feat: add support for external connection poolers

* chore: use .env symlink in database package

* chore: remove database .env.example

* docs: finish supabase pooling section

* docs: fix small typo

* docs: replace prisma with app wording

* docs: fix up supabase pooling section

* fix: deleted too much in contributing docs

* docs: change pooling heading

* docs: reduce linux shaming

* docs: include direct connection url in fly.io
2023-08-11 15:51:54 +01:00
github-actions[bot] beef7769c6 chore: Update version for release (#304)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-11 15:48:41 +01:00
Eric Allam e9088cf08c Supabase quickstart guide (#311)
* Added docs images for supabase

* Compressed some existing images

* Improved some of the main quickstart wording

* First draft of the Supabase quickstart

* Tweaks to the Supabase quickstart and the supabase integration docs

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-08-11 15:29:53 +01:00
Eric Allam 390bed0b9a Some tweaks to the Event Filter guide 2023-08-11 14:38:43 +01:00
Eric Allam e3e3dab525 Fixed missing } in the express package 2023-08-11 14:34:31 +01:00
Chigala b884a0ab53 doc: added a new documentation for event filters (#300)
* doc: added a new documentation for event filters

* removed typos from the event filter doc and formatted it better.

* made new changes

- moved the eventfilter doc to the guides section
- linked the old eventfilter doc to the new one

* updated the eventfilter doc based on the requested changes
2023-08-11 14:25:04 +01:00
Liran Tal 871d6e1d52 fix: support express jsonParser middleware if used in the Express app (#309)
* fix: support express jsonParser middleware if used in the Express app

* Create sixty-windows-run.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-11 14:23:29 +01:00
Liran Tal 7daa82fc7a fix: avoid crashing the Node.js app on uncaught exceptions (#307)
* fix: avoid crashing the Node.js app on uncaught exceptions

* Create smooth-dodos-obey.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-11 14:18:31 +01:00
Eric Allam 9dd7fb2748 Improved the onboarding experience
- Automatically refresh the jobs page when there are no jobs
 - Celebrate 🎊
2023-08-11 14:08:39 +01:00
James Ritchie c246578a81 Made the CLI error more helpful if you’re not running your Next.js app 2023-08-11 11:24:52 +01:00
Chigala d395b95762 fix: Handle ngrok config upgrade error in createTunnel function (#295)
* fix: Handle ngrok config upgrade error in createTunnel function

* Improved the output when upgrading the ngrok configuration

* Create three-flies-sneeze.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-11 10:39:58 +01:00
Eric Allam f3bda59617 Fixed the sendgrid tsconfig 2023-08-11 10:16:24 +01:00
Eric Allam ef8b52c20f Fixed outdated lockfile issue 2023-08-11 10:15:08 +01:00
Chigala 2bedc779b9 feature: initial work for sendgrid integration (#264)
* feature: initial work for sendgrid integration

* fix: fixed the ts-error and added sendgrid integration to the job-catalogue

* SendGrid added to dependencies

* Made the from email an env var so it can be tested by non-Trigger.dev team members

* Refactor: Modify sendEmail function to return void and remove SendEmailResponse type

Co-authored-by: Matt Aitken <matt@mattaitken.com>

* Create funny-kangaroos-glow.md

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-11 10:12:19 +01:00
Eric Allam a5b5912270 Change the default max concurrent runs from 100 to 1000
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-10 17:21:37 +01:00
D-K-P 275318d2df Docs updates, including added Supabase to the examples table 2023-08-10 14:54:03 +01:00
nicktrn 8d48ebdee8 docs: supabase guide without pooling section (#299)
* docs: add supabase guide

* docs: add supabase to mint.json

* docs: change supabase schema from public

* docs: supabase pooling coming soon
2023-08-10 14:16:54 +01:00
Eric Allam 7ef0fe79b5 Update manual.mdx - spelling mistake 2023-08-10 11:21:06 +01:00
Yaswanth 921957958e Adds Missing syntax highlight to manual setup documentation (#298) 2023-08-10 10:30:22 +01:00
Eric Allam 73721fc55c Remove private beta mention in the docs 2023-08-10 09:33:50 +01:00
Eric Allam 4fcec3bd0d Organized imports
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-09 21:19:57 +01:00
Eric Allam 7f4b6b9974 Remove the invite code guard 2023-08-09 21:19:25 +01:00
Eric Allam 950ab24e7f Add the placeholder supabase quickstart 2023-08-09 21:14:34 +01:00
James Ritchie c4da1b4a9a Updated the discord link
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-09 16:09:33 +01:00
Eric Allam 24debf0cee Slight change to the supabase job catalog 2023-08-09 16:02:31 +01:00
James Ritchie 645643ea41 Improved onboarding (#292)
* Only show the Example Jobs list when you have 1 job

* Updated the logic for displaying the different information depending on the no. of jobs you have

* Removed some unnecessary markup

* Clicking on a radio option toggles the content

* Improved the centering of the help panel

* Conetent updates to the 2 onboarding paths

* Added a prompt so you click through to run your job

* More content updates to the help panel

* Only show the prompt if the Job has never been run

* Added back in the help panel which shows the examples table if you have > 0 jobs

* Added a dialog for the I am Stuck button

* Added the new ‘new-next-app’ text for the cli command

* Refactored the ternary logic so it’s simpler

* Removed I’m stuck button and cleaned up imports

* Added in feedback button

* Added margin bottom to the Callout on the Test popover

* WIP adding a new radio button variant that takes an icon

* Added a new step to create a new next app

* The state of the tabs is stored as a URL param

* Small shadow tweak to the side panel

* Added a Discord button to the feedback menu

* Added new icons to the Named icons component

* Added a gradient to the background of the jobs page

* Made the icons take on color outside of component

* Added another NamedIcon for tree

* Fixed number

* Improved some of the typography styles

* Updated the icons

* Fixed issue with radio buttons alignment

* Added a missing step to the new next app onboarding

* Refactored example code and added all the examples to the panel

* Small icon update

* Made the title white again

* Added an icon to the storybook radiogroup

* Added a badges storybook

* Chevron table cell now takes children

* import cleanup

* Changed the background image from an arbirary value to an import

* Added a NEW badge which shows if you have a new job

* Improved the copy on the Examples help panel and updated some of the layout

* Removed defaultOpen on the help panel

* Stop the icons getting smaller

---------

Co-authored-by: D-K-P <dkp.github@pm.me>
2023-08-09 15:00:08 +01:00
Eric Allam 321378db4f Run pnpm i to fix lock file
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-09 12:40:48 +01:00
github-actions[bot] c8b10ce188 chore: Update version for release (#291)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-09 12:40:15 +01:00
Eric Allam 57ed2b2d0d Fixed cli type errors 2023-08-09 12:37:22 +01:00
Eric Allam fa3a22eb78 Added an $isNull EventFilter condition matcher 2023-08-09 12:35:47 +01:00
Dan a1bc15d18b Updated the example job (#288)
* Changed the example job to include a delay

* Improved the example job copy and added ‘in a separate terminal’ to 2.

* Create kind-eggs-pump.md

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-09 12:21:46 +01:00
Yaswanth 3f48dd0a68 [TRI-945]: Adds manual project setup guide to the docs (#286)
Co-authored-by: Crossover <crossover@MacBook-Pro.local>
2023-08-09 12:18:05 +01:00
Eric Allam 5c71868a73 Fix the examples in the supabase integration 2023-08-09 12:16:27 +01:00
Chigala 3ce6decef4 Feat/scaffold job catalog file (#283)
* feat: adds a job catalog file when an integration is created via the CLI

* added a changeset

* fix: create-integration CLI throws an error when it tries to install dependencies

* Update getUserPkgManager.ts to main

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-09 10:42:20 +01:00
Eric Allam ab007f2708 Fix the CLI typecheck error 2023-08-09 10:41:06 +01:00
Eric Allam 8412680863 Adding createImageEdit and createImageVariation tasks to openai 2023-08-09 10:39:17 +01:00
Matt Aitken a90908df6e CLI init fixes: don't ask for endpoint slug, fix for next.config and package manager artifact issue (#287)
* If an endpointId isn’t in the arguments or package, then use the project slug. Don’t ask for it.

* Should await the package artifact detection

* Catch error thrown if there’s no next.config file when detecting the type of Next project

* Create long-carrots-camp.md
2023-08-09 10:37:10 +01:00
Eric Allam a82b86210f Fix pnpm lock file for package release 2023-08-08 22:08:40 +01:00
github-actions[bot] 0367a92731 chore: Update version for release (#281)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-08 22:08:12 +01:00
Eric Allam d230cf622f Treat @trigger.dev/sdk the same as @trigger.dev/core in the webapp
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-08 15:52:14 +01:00
Eric Allam a1bf073b43 Trigger another release run 2023-08-08 15:18:13 +01:00
Eric Allam 5588108b97 Add unit test running to the public CI 2023-08-08 15:04:15 +01:00
Eric Allam b9bcecffe3 Added some Supabase storage and auth triggers to the job catalog 2023-08-08 15:02:07 +01:00
Eric Allam 59075f5fc2 EventFilter now supports more complex condition filters #271 2023-08-08 15:02:07 +01:00
Sai Hari fa942fc9f5 feat: 🎸 add end-to-end test harness (#253)
* feat: 🎸 add end-to-end test harness

* Address PR comments
2023-08-08 13:55:25 +01:00
D-K-P 47a8a557cf Replaced all the screenshots of the environments page 2023-08-08 11:47:25 +01:00
Yaswanth b51d0b5399 Fix:[TRI-879]-Hides help panel when endpoints exists on the Environme… (#256)
* Fix:[TRI-879]-Hides help panel when endpoints exists on the Environments & API Keys page

* fix: adds check for fully configured client

g

---------

Co-authored-by: Crossover <crossover@MacBook-Pro.local>
2023-08-08 11:15:58 +01:00
D-K-P 763c84e3b3 Replaced api key screenshot and text 2023-08-08 10:41:33 +01:00
Eric Allam dfb00e84e9 Fixing pnpm lock file 2023-08-08 07:35:25 +01:00
github-actions[bot] b5a64545bb chore: Update version for release (#273)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-08 07:32:24 +01:00
Eric Allam 3cf3eaff48 @trigger.dev/supabase: You can now trigger on multiple database events in the same job 2023-08-07 17:27:49 +01:00
Matt Aitken 4ca758a3de job-catalog: added initial setup and CLI build step
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-07 15:07:58 +01:00
Matt Aitken da81537009 Update job-catalog instructions
`pnpm run trigger:dev` should be `pnpm run dev:trigger`
2023-08-07 14:57:08 +01:00
Eric Allam 7c1b13ba3b Increase webhook delivery attempts to the max 25 attempts over 3 days 2023-08-07 14:25:23 +01:00
D-K-P 4b654e5b3b Fixed broken docs link 2023-08-07 13:29:11 +01:00
Eric Allam 726a50ace7 Point to the workspace version of @trigger.dev/sdk 2023-08-07 13:17:36 +01:00
github-actions[bot] 9deffb67c4 chore: Update version for release (#267)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-07 13:08:27 +01:00
Matt Aitken ff04bf44ee CLI: detect package manager using artifacts if they exist (#261)
* Detect package manager from artifacts (e.g. package-lock.json) and fallback to process.env.npm_config_user_agent

* Added a changeset
2023-08-07 13:01:22 +01:00
Matt Aitken e740297829 Cli: improve Next.js project detection (#262)
* Detect presence of a “next” dependency

* Use a strict undefined check instead

* Changeset: Detect Next.js project by looking at dependencies, not next.config.js

* Read a package json file

* First check for next.config file, otherwise use next dependency

* Update changeset description
2023-08-07 13:01:03 +01:00
D-K-P a31705e198 Added text summarizer / github issue reminder jobs to the examples table 2023-08-07 12:58:43 +01:00
Eric Allam f0bdf53364 Auto index production endpoints every 10 minutes 2023-08-07 12:49:37 +01:00
Matt Aitken 9638499163 Add your first endpoint in the dashboard (#269)
* Created the sheet

* Started work on the resource route

* Created ValidateCreateEndpointService and the EndpointValidateApi (name TBC)

* The TriggerClient responds with the id

* Tidied some stuff up

* Alternative: EndpointApi doesn’t take endpointSlug. Instead just Ping() does

* Create cool-snakes-deny.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-07 11:28:34 +01:00
Matt Aitken 9104c252b7 Telemetry (new user) (#268)
* Removed old telemetry

* Started adding TriggerClient telemetry

* Added a log if the telemetry client is created, event renamed to “user.created”
2023-08-07 11:28:13 +01:00
Matt Aitken 72cf345602 Added “thumbsRating” to docs pages 2023-08-07 10:50:02 +01:00
jemiluv8 d5b8f8299d update cli init to create index file in examples folder (#225)
* update cli init to create index file in examples folder

* add patch changeset

* Create silly-baboons-join.md

* remove comment in router file
add comment in jobs index to instruct them to

* rename examplesIndex to examplesIndexFileName

* import jobs module in routeContent for createTriggerPageRoute just as we did for createTriggerAppRoute

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-06 16:42:32 +01:00
Matt Aitken dd22b88d25 Latest lockfile… 🙄 2023-08-03 22:52:46 +01:00
github-actions[bot] 62b9c5879c chore: Update version for release (#248) 2023-08-03 22:47:23 +01:00
Eric Allam 1b0973fbc1 Fixed error when configuring a new endpoint failed and fixed ping to now throw an error when parsing JSON
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-03 16:02:16 +01:00
Eric Allam fc78854ed4 @trigger.dev/cli: Don't require the TRIGGER_API_URL to be set (it has a default) 2023-08-03 14:49:46 +01:00
Eric Allam ed15bb0618 Another small supabase tweak 2023-08-03 13:09:56 +01:00
Eric Allam ced9034428 Another small supabase doc fix 2023-08-03 13:08:42 +01:00
Eric Allam 7ec329d5d6 Fixed broken image in supabase docs 2023-08-03 13:06:23 +01:00
Eric Allam 81180999de The dev command should use a POST request when doing the PING to the local server 2023-08-03 13:01:08 +01:00
Eric Allam feaa79ecf3 @trigger.dev/supabase: Automatically enable database webhooks when using triggers 2023-08-03 13:00:29 +01:00
Eric Allam d34dc0847c More OpenAI examples 2023-08-03 09:55:07 +01:00
Eric Allam 1b7c7520b4 new Job -> client.defineJob 2023-08-03 09:53:15 +01:00
Eric Allam 34d77e830c Incorporate improved OpenAI docs from old repo 2023-08-03 09:51:25 +01:00
Eric Allam 87c302bf26 Document how to enable supabase database webhooks 2023-08-03 09:39:58 +01:00
Eric Allam d462c901f5 A couple of quickstart tweaks 2023-08-02 17:16:39 +01:00
D-K-P 55c3d79cc8 Deleted the info box on the integration pages 2023-08-02 17:11:41 +01:00
Eric Allam ce901c92e4 Update @trigger.dev/supabase to the latest supabase-management-js 2023-08-02 17:01:02 +01:00
393 changed files with 19570 additions and 3110 deletions
+17 -4
View File
@@ -1,11 +1,24 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
"changelog": "@changesets/cli/changelog",
"changelog": [
"@remix-run/changelog-github",
{
"repo": "triggerdotdev/trigger.dev"
}
],
"commit": false,
"fixed": [["@trigger.dev/*"]],
"fixed": [
[
"@trigger.dev/*"
]
],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["webapp", "emails", "@trigger.dev/database"]
}
"ignore": [
"webapp",
"emails",
"@trigger.dev/database"
]
}
+4 -1
View File
@@ -3,7 +3,10 @@ SESSION_SECRET=abcdef1234
MAGIC_LINK_SECRET=abcdef1234
ENCRYPTION_KEY=ae13021afef0819c3a307ad487071c06 # Must be a random 16 byte hex string. You can generate an encryption key by running `openssl rand -hex 16` in your terminal
LOGIN_ORIGIN=http://localhost:3030
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public
# This sets the URL used for direct connections to the database and should only be needed in limited circumstances
# See: https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#fields:~:text=the%20shadow%20database.-,directUrl,-No
DIRECT_URL=${DATABASE_URL}
REMIX_APP_PORT=3030
APP_ENV=development
APP_ORIGIN=http://localhost:3030
+100 -1
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- improvements/*
tags:
- "v.docker.*"
paths:
@@ -64,13 +65,107 @@ jobs:
- name: 🔎 Type check
run: pnpm run typecheck --filter webapp
unitTests:
name: Unit Tests
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7.18
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: Run Unit Tests
run: |
pnpm run test
e2e:
name: e2e Tests
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: 🐳 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7.18
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: |
# Setup environment variables
cp ./.env.example ./.env
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
# Build packages
pnpm run build --filter @examples/nextjs-test^...
pnpm --filter @trigger.dev/database generate
# Move trigger-cli bin to correct place
pnpm install --frozen-lockfile
# Execute tests
pnpm run docker
pnpm run db:migrate
pnpm run db:seed
pnpm run test:e2e
# Cleanup
pnpm run docker:stop
- name: Upload Playwright report
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
publish:
needs: [typecheck]
needs: [typecheck, unitTests, e2e]
runs-on: buildjet-4vcpu-ubuntu-2204
outputs:
version: ${{ steps.get_version.outputs.version }}
short_sha: ${{ steps.get_commit.outputs.sha_short }}
steps:
- name: 🐳 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
@@ -84,6 +179,10 @@ jobs:
IMAGE_TAG="v${ORIGINAL_VERSION}"
fi
echo "IMAGE_TAG=${IMAGE_TAG}"
elif [[ $GITHUB_REF == refs/heads/improvements/* ]]; then
ORIGINAL_VERSION="${GITHUB_REF#refs/heads/improvements/}"
IMAGE_TAG="${ORIGINAL_VERSION}.rc"
echo "IMAGE_TAG=${IMAGE_TAG}"
elif [[ $GITHUB_REF == refs/heads/* ]]; then
IMAGE_TAG="${GITHUB_REF#refs/heads/}"
echo "IMAGE_TAG=${IMAGE_TAG}"
+4 -1
View File
@@ -48,4 +48,7 @@ apps/**/public/build
.sentryclirc
.buildt
**/tmp/
**/tmp/
/test-results/
/playwright-report/
/playwright/.cache/
+63 -13
View File
@@ -27,7 +27,7 @@ branch are tagged into a release monthly.
```
> If you are on windows, run the following command on gitbash with admin privileges:
> `git clone -c core.symlinks=true https://triggerdotdev/trigger.dev.git`
> `git clone -c core.symlinks=true https://github.com/triggerdotdev/trigger.dev.git`
2. Navigate to the project folder
```
@@ -37,11 +37,11 @@ branch are tagged into a release monthly.
```
pnpm i
```
4. Create your `.env` files
4. Create your `.env` file
```
cp .env.example .env && cp packages/database/.env.example packages/database/.env
cp .env.example .env
```
5. Open the root `.env` file and generate a new value for `ENCRYPTION_KEY`:
5. Open it and generate a new value for `ENCRYPTION_KEY`:
`ENCRYPTION_KEY` is used to two-way encrypt OAuth access tokens and so you'll probably want to actually generate a unique value, and it must be a random 16 byte hex string. You can generate one with the following command:
@@ -136,38 +136,88 @@ pnpm run dev
3. Create a new temporary Next.js app in examples directory
```sh
pnpm create next-app@latest
cd ./examples
pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir --import-alias "@/*"
```
Follow the prompts to create a TypeScript project using the App Directory.
4. Then once that's finished, add the `@trigger.dev/cli` to the `devDependencies` of the newly created Next.js app's `package.json` file, like so:
```json
{
// other package.json properties
"devDependencies": { "@trigger.dev/cli": "workspace:*" }
}
```
5. Open a new terminal window, navigate into the example, and initialize the CLI:
5. Back in the terminal, navigate into the example, and initialize the CLI. When prompted, select `self-hosted` and enter `localhost:3030` if you are testing against the local instance of Trigger.dev, or you can just use the Trigger.dev cloud. When asked for an API key, use the key you copied earlier.
```sh
cd examples/your-newly-created-nextjs-project
cd ./test-cli
pnpm i
pnpm exec trigger-cli init
```
6. When prompted, select `self-hosted` and enter `localhost:3030` for your local version of the webapp. When asked for an API key, use the key you copied earlier.
6. If you are just testing the `init` command, you can stop here. If you'd like to test the `dev` command, first start the Next.js app on port 3000:
7. Run the CLI
```sh
pnpm run dev
```
7. Open a new terminal window, and then run the `dev` command like so:
```sh
pnpm exec trigger-cli dev
```
8. After running the CLI, start your newly created Next.js project. You should now be able to see the changes.
8. Please remember to delete the temporary project you created after you've tested the changes, and before you raise a PR.
9. Please remember to delete the temporary project you created after you've tested the changes, and before you raise a PR.
## Running end-to-end webapp tests
To run the end-to-end tests, follow the steps below:
1. Set up environment variables (copy example envs into the correct place)
```sh
cp ./.env.example ./.env
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
```
2. Set up dependencies
```sh
# Build packages
pnpm run build --filter @examples/nextjs-test^...
pnpm --filter @trigger.dev/database generate
# Move trigger-cli bin to correct place
pnpm install --frozen-lockfile
# Install playwrite browsers (ONE TIME ONLY)
npx playwright install
```
3. Set up the database
```sh
pnpm run docker
pnpm run db:migrate
pnpm run db:seed
```
4. Run the end-to-end tests
```sh
pnpm run test:e2e
```
### Cleanup
The end-to-end tests use a `setup` and `teardown` script to seed the database with test data. If the test runner doesn't exit cleanly, then the database can be left in a state where the tests can't run because the `setup` script will try to create data that already exists. If this happens, you can manually delete the `users` and `organizations` from the database using prisma studio:
```sh
# With the database running (i.e. pnpm run docker)
pnpm run db:studio
```
## Add sample jobs
+157 -157
View File
@@ -2,180 +2,180 @@
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
@@ -186,16 +186,16 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright [2023] [Trigger.dev]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+1 -1
View File
@@ -62,4 +62,4 @@ We provide an official trigger.dev docker image you can use to easily self-host
## Development
To setup and develop locally or contribute to the open source project, follow our [developement guide](./CONTRIBUTING.md).
To setup and develop locally or contribute to the open source project, follow our [development guide](./CONTRIBUTING.md).
+15
View File
@@ -0,0 +1,15 @@
import { ApiEventLog } from "@trigger.dev/core";
import { EventRecord } from "@trigger.dev/database";
export function eventRecordToApiJson(eventRecord: EventRecord): ApiEventLog {
return {
id: eventRecord.eventId,
name: eventRecord.name,
payload: eventRecord.payload as any,
context: eventRecord.context as any,
timestamp: eventRecord.timestamp,
deliverAt: eventRecord.deliverAt,
deliveredAt: eventRecord.deliveredAt,
cancelledAt: eventRecord.cancelledAt,
};
}
@@ -0,0 +1,161 @@
export function OneTreeIcon({ className }: { className?: string }) {
return (
<svg
className={className}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_4914_58397)">
<g filter="url(#filter0_d_4914_58397)">
<path
d="M9 14H11C11 14 11 15 11 17C11 19 11.5 20 11.5 20H8.5C8.5 20 9 19 9 17C9 15 9 14 9 14Z"
fill="url(#paint0_linear_4914_58397)"
/>
</g>
<g filter="url(#filter1_d_4914_58397)">
<circle cx="5.5" cy="8.5" r="3.5" fill="currentColor" />
</g>
<g filter="url(#filter2_d_4914_58397)">
<circle cx="8" cy="12" r="3" fill="currentColor" />
</g>
<circle cx="13" cy="11" r="4" fill="currentColor" />
<circle cx="15.5" cy="7.5" r="2.5" fill="currentColor" />
<g filter="url(#filter3_d_4914_58397)">
<circle cx="9" cy="6" r="4" fill="currentColor" />
</g>
<circle cx="12.5" cy="5.5" r="3.5" fill="currentColor" />
</g>
<defs>
<filter
id="filter0_d_4914_58397"
x="4.5"
y="14"
width="11"
height="14"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4914_58397"
result="shape"
/>
</filter>
<filter
id="filter1_d_4914_58397"
x="-2"
y="5"
width="15"
height="15"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4914_58397"
result="shape"
/>
</filter>
<filter
id="filter2_d_4914_58397"
x="1"
y="9"
width="14"
height="14"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4914_58397"
result="shape"
/>
</filter>
<filter
id="filter3_d_4914_58397"
x="1"
y="-2"
width="16"
height="16"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4914_58397"
result="shape"
/>
</filter>
<linearGradient
id="paint0_linear_4914_58397"
x1="10"
y1="14"
x2="10"
y2="20"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#402309" />
<stop offset="1" stopColor="#713F12" />
</linearGradient>
<clipPath id="clip0_4914_58397">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
);
}
@@ -0,0 +1,137 @@
export function SaplingIcon({ className }: { className?: string }) {
return (
<svg
className={className}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_4906_23816)">
<path
d="M4.39538 17.7403C7.25332 16.3563 13.2867 16.4291 15.5096 17.7403C16.6666 18.4227 17.2938 19.0069 17.6294 19.4103C17.8542 19.6806 17.651 20 17.2994 20H2.34215C2.151 20 1.9749 19.8835 2.01519 19.6967C2.09465 19.3281 2.53308 18.6421 4.39538 17.7403Z"
fill="url(#paint0_linear_4906_23816)"
/>
<g filter="url(#filter0_d_4906_23816)">
<path
d="M9.37762 9.80872C7.43627 7.16364 3.49903 8.13783 2.43836 8.66818C2.26159 9.02174 3.32907 8.80396 4.08851 10.8279C4.84794 12.8519 7.63503 13.1864 8.74087 12.0806C9.27121 11.5502 9.69141 10.2363 9.37762 9.80872Z"
fill="currentColor"
/>
</g>
<g filter="url(#filter1_d_4906_23816)">
<path
d="M9.03577 9.94522L9.99862 9.80223C9.99862 9.80223 10.0387 16.2908 10.9485 16.8179C11.8582 17.345 8.47933 17.1309 8.92618 16.8179C9.37303 16.505 9.03577 9.94522 9.03577 9.94522Z"
fill="currentColor"
/>
</g>
<g filter="url(#filter2_d_4906_23816)">
<path
d="M9.36235 10.1127C9.36235 5.78131 13.3229 4.41473 15.9395 4.00389C16.1892 3.96468 16.3818 4.22916 16.2961 4.46698C16.0763 5.07608 15.7914 6.09975 15.6717 7.46762C15.5019 9.40897 14.8971 10.3478 14.3613 10.962C13.3664 12.1026 9.36235 12.2341 9.36235 10.1127Z"
fill="currentColor"
/>
</g>
</g>
<defs>
<filter
id="filter0_d_4906_23816"
x="-1.58107"
y="8.04684"
width="15.0664"
height="12.6571"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4906_23816"
result="shape"
/>
</filter>
<filter
id="filter1_d_4906_23816"
x="4.88525"
y="9.80223"
width="10.2195"
height="15.3357"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4906_23816"
result="shape"
/>
</filter>
<filter
id="filter2_d_4906_23816"
x="5.36235"
y="4"
width="14.9544"
height="15.7756"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4906_23816"
result="shape"
/>
</filter>
<linearGradient
id="paint0_linear_4906_23816"
x1="10"
y1="16.7294"
x2="10"
y2="20"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.291667" stopColor="#402309" />
<stop offset="1" stopColor="#713F12" />
</linearGradient>
<clipPath id="clip0_4906_23816">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
);
}
@@ -0,0 +1,44 @@
export function TwoTreesIcon({ className }: { className?: string }) {
return (
<svg
className={className}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_4906_58417)">
<path
d="M10 20V17"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.7239 16.3L14.8274 13H15.1171C15.3061 12.9973 15.4901 12.9371 15.6464 12.8271C15.8027 12.717 15.9243 12.5619 15.9963 12.3808C16.0683 12.1998 16.0873 12.001 16.0512 11.8088C16.015 11.6167 15.9252 11.4398 15.7929 11.3L12.8965 8H13.0896C13.2868 8.01843 13.4847 7.97361 13.6565 7.87162C13.8282 7.76963 13.9656 7.6154 14.0499 7.42984C14.1342 7.24428 14.1614 7.03636 14.1278 6.83425C14.0942 6.63215 14.0014 6.44564 13.862 6.3L10 2L7.29699 5.0096C8.25755 5.0792 9.16726 5.48607 9.85673 6.16512C10.5124 6.81087 10.9224 7.65741 11.023 8.56163C11.6295 8.91846 12.136 9.42887 12.4856 10.0473C12.9492 10.8671 13.1066 11.8229 12.9286 12.7472C12.7507 13.6714 12.2496 14.5014 11.5168 15.0945C10.8041 15.6712 9.91568 15.9888 8.99914 15.999V18H17.0481C17.237 17.9973 17.4211 17.9371 17.5774 17.8271C17.7337 17.717 17.8553 17.5618 17.9273 17.3808C17.9992 17.1998 18.0183 17.001 17.9822 16.8088C17.946 16.6167 17.8562 16.4398 17.7239 16.3ZM7.99914 18V15.9991H6.00231V18H7.99914ZM4.97036 15.9991H5.00231L5.00231 18H2.95193C2.76296 17.9973 2.57892 17.9371 2.42263 17.8271C2.26635 17.717 2.14468 17.5618 2.07272 17.3808C2.00076 17.1998 1.98168 17.001 2.01783 16.8088C2.05398 16.6167 2.14377 16.4398 2.27609 16.3L3.01417 15.4591C3.59927 15.795 4.26325 15.9821 4.94587 15.9988C4.95403 15.999 4.9622 15.9991 4.97036 15.9991ZM3.69171 14.6872C4.08702 14.8804 4.52307 14.9881 4.97036 14.9991H8.93032C9.64375 15.0045 10.3365 14.7632 10.8877 14.3171C11.4389 13.871 11.8137 13.2485 11.9466 12.5581C12.0796 11.8678 11.9623 11.1534 11.6151 10.5395C11.268 9.9256 10.7131 9.45119 10.0472 9.19898V8.99898C10.0472 8.20331 9.7263 7.44023 9.15504 6.87761C8.58378 6.31499 7.80899 5.99891 7.00111 5.99891C6.77881 5.99891 6.55901 6.02285 6.34528 6.06926L6.13804 6.3C5.99864 6.44564 5.90584 6.63215 5.87222 6.83425C5.8386 7.03636 5.86579 7.24428 5.9501 7.42984C6.03441 7.6154 6.17176 7.76963 6.34354 7.87162C6.51533 7.97361 6.71323 8.01843 6.91043 8H7.10353L4.20706 11.3C4.07475 11.4398 3.98496 11.6167 3.94881 11.8088C3.91266 12.001 3.93174 12.1998 4.0037 12.3808C4.07566 12.5619 4.19732 12.717 4.35361 12.8271C4.5099 12.9371 4.69394 12.9973 4.88291 13H5.17255L3.69171 14.6872Z"
fill="currentColor"
/>
<path
d="M7 20L7 13.9814"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.0472 9V9.2C10.7131 9.4522 11.268 9.92661 11.6151 10.5405C11.9623 11.1544 12.0796 11.8687 11.9466 12.5591C11.8137 13.2494 11.4389 13.8719 10.8877 14.318C10.3365 14.7641 9.64374 15.0054 8.93032 15H4.97036C4.26546 14.9827 3.58847 14.7251 3.05487 14.2712C2.52126 13.8172 2.1641 13.195 2.0443 12.5106C1.92449 11.8263 2.04946 11.1222 2.3979 10.5185C2.74634 9.91474 3.29666 9.44875 3.95499 9.2V9C3.95499 8.20435 4.27592 7.44129 4.84718 6.87868C5.41843 6.31607 6.19323 6 7.00111 6C7.80899 6 8.58378 6.31607 9.15504 6.87868C9.7263 7.44129 10.0472 8.20435 10.0472 9Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_4906_58417">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

@@ -0,0 +1,57 @@
import { cn } from "~/utils/cn";
const variant = {
small:
"py-[0.25rem] px-1.5 text-xxs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-[0.125rem]",
normal:
"py-1 px-1.5 text-xs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-sm",
};
type ActiveBadgeProps = {
active: boolean;
className?: string;
badgeSize?: keyof typeof variant;
};
export function ActiveBadge({ active, className, badgeSize = "normal" }: ActiveBadgeProps) {
switch (active) {
case true:
return (
<span className={cn(variant[badgeSize], "bg-slate-800 text-green-500", className)}>
Active
</span>
);
case false:
return (
<span className={cn(variant[badgeSize], "bg-slate-800 text-dimmed", className)}>
Disabled
</span>
);
}
}
export function MissingIntegrationBadge({
className,
badgeSize = "normal",
}: {
className?: string;
badgeSize?: keyof typeof variant;
}) {
return (
<span className={cn(variant[badgeSize], "bg-rose-600 text-white", className)}>
Missing Integration
</span>
);
}
export function NewBadge({
className,
badgeSize = "normal",
}: {
className?: string;
badgeSize?: keyof typeof variant;
}) {
return (
<span className={cn(variant[badgeSize], "bg-green-600 text-background", className)}>New!</span>
);
}
+34 -6
View File
@@ -20,6 +20,8 @@ import {
} from "./primitives/Select";
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "./primitives/Sheet";
import { TextArea } from "./primitives/TextArea";
import { DiscordIcon } from "@trigger.dev/companyicons";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
type FeedbackProps = {
button: ReactNode;
@@ -55,14 +57,15 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
<SheetContent size="sm">
<SheetHeader className="justify-between">Help & feedback</SheetHeader>
<SheetBody>
<Paragraph variant="small" className="mb-4">
Use this form to ask for help or give us feedback. We read every message and will get
<DiscordBanner />
<Paragraph variant="small" className="mb-4 border-t border-slate-800 pt-3">
Or use this form to ask for help or give us feedback. We read every message and will get
back to you as soon as we can.
</Paragraph>
<Form method="post" action="/resources/feedback" {...form.props}>
<Fieldset>
<Fieldset className="max-w-full">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<InputGroup>
<InputGroup className="max-w-full">
<Label>How can we help?</Label>
<SelectGroup>
<Select {...conform.input(feedbackType)} defaultValue={defaultValue}>
@@ -80,14 +83,14 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
</SelectGroup>
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
</InputGroup>
<InputGroup>
<InputGroup className="max-w-full">
<Label>Message</Label>
<TextArea {...conform.textarea(message)} />
<FormError id={message.errorId}>{message.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<FormButtons
className="max-w-md"
className="w-full"
confirmButton={
<Button type="submit" variant="primary/medium">
Send
@@ -101,3 +104,28 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
</Sheet>
);
}
function DiscordBanner() {
return (
<a
href="https://discord.gg/nkqV9xBYWy"
target="_blank"
className="group mb-4 flex w-full items-center justify-between rounded-md border border-slate-600 bg-gradient-to-br from-blue-400/30 to-indigo-400/50 p-4 transition hover:border-indigo-400"
>
<div className="flex flex-col gap-y-2">
<DiscordIcon className="h-8 w-8" />
<h2 className="font-title text-2xl text-bright transition group-hover:text-white">
Join the Trigger.dev
<br />
Discord community
</h2>
<Paragraph variant="small">
Get help or answer questions from the Trigger.dev community.
</Paragraph>
</div>
<div className="h-full">
<ChevronRightIcon className="h-5 w-5 text-slate-400 transition group-hover:translate-x-1 group-hover:text-indigo-400" />
</div>
</a>
);
}
@@ -0,0 +1,54 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import {
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { EnvironmentLabel } from "./environments/EnvironmentLabel";
import { DateTime } from "./primitives/DateTime";
import { ActiveBadge } from "./ActiveBadge";
export type JobEnvironment = {
type: RuntimeEnvironmentType;
lastRun?: Date;
version: string;
enabled: boolean;
};
type JobStatusTableProps = {
environments: JobEnvironment[];
};
export function JobStatusTable({ environments }: JobStatusTableProps) {
return (
<Table fullWidth>
<TableHeader>
<TableRow>
<TableHeaderCell>Env</TableHeaderCell>
<TableHeaderCell>Last Run</TableHeaderCell>
<TableHeaderCell alignment="right">Version</TableHeaderCell>
<TableHeaderCell alignment="right">Status</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{environments.map((environment, index) => (
<TableRow key={index}>
<TableCell>
<EnvironmentLabel environment={environment} />
</TableCell>
<TableCell>
{environment.lastRun ? <DateTime date={environment.lastRun} /> : "Never Run"}
</TableCell>
<TableCell alignment="right">{environment.version}</TableCell>
<TableCell alignment="right">
<ActiveBadge active={environment.enabled} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
@@ -13,7 +13,7 @@ export function EnvironmentLabel({
return (
<span
className={cn(
"flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
"inline-flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
environmentColorClassName(environment),
className
)}
@@ -1,4 +1,9 @@
import { Link } from "@remix-run/react";
import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid";
import { Link, useRevalidator } from "@remix-run/react";
import { useEffect, useState } from "react";
import { useEventSource } from "remix-utils";
import invariant from "tiny-invariant";
import gradientBackground from "~/assets/images/gradient-background.png";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
@@ -7,7 +12,8 @@ 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 { jobTestPath } from "~/utils/pathBuilder";
import { jobTestPath, projectStreamingPath } from "~/utils/pathBuilder";
import { Feedback } from "../Feedback";
import { CodeBlock } from "../code/CodeBlock";
import { InlineCode } from "../code/InlineCode";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
@@ -23,132 +29,238 @@ import {
ClientTabsTrigger,
} from "../primitives/ClientTabs";
import { ClipboardField } from "../primitives/ClipboardField";
import { Header2 } from "../primitives/Headers";
import { Header1, Header2 } from "../primitives/Headers";
import { NamedIcon } from "../primitives/NamedIcon";
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
import { TextLink } from "../primitives/TextLink";
import integrationButton from "./integration-button.png";
import selectEnvironment from "./select-environment.png";
import selectExample from "./select-example.png";
type SelectionChoices = "use-existing-project" | "create-new-next-app";
export function HowToSetupYourProject() {
const project = useProject();
const devEnvironment = useDevEnvironment();
const appOrigin = useAppOrigin();
return (
<>
<StepNumber stepNumber="1" title="Run the CLI 'init' command in a Next.js project" />
<StepContentContainer>
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`npx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`npx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`pnpm dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`pnpm dlx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`yarn dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`yarn dlx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
</ClientTabs>
<Paragraph spacing>
Youll notice a new folder in your project called 'jobs'. Weve added a very simple
example Job in <InlineCode>examples.ts</InlineCode> to help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Next.js app" />
<StepContentContainer>
<Paragraph>Ensure your app is running locally.</Paragraph>
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`npm run dev`} />
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`pnpm run dev`} />
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`yarn run dev`} />
</ClientTabsContent>
</ClientTabs>
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
<StepContentContainer>
<Paragraph spacing>
In a <strong className="text-bright">separate terminal window or tab</strong> run:
</Paragraph>
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`npx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
</ClientTabs>
<Paragraph spacing variant="small">
If youre not running on port 3000 you can specify the port by adding{" "}
<InlineCode>--port 3001</InlineCode> to the end.
</Paragraph>
<Paragraph spacing variant="small">
You should leave the <InlineCode>dev</InlineCode> command running when you're developing.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="4" title="Check for Jobs" />
<StepContentContainer>
<Paragraph>
Once you've run the CLI command, click Refresh to view your example Job in the list.
</Paragraph>
<Button
variant="primary/medium"
className="mt-4"
LeadingIcon="refresh"
onClick={() => window.location.reload()}
const [selectedValue, setSelectedValue] = useState<SelectionChoices | null>(null);
invariant(devEnvironment, "devEnvironment is required");
const revalidator = useRevalidator();
const events = useEventSource(projectStreamingPath(project.id), {
event: "message",
});
useEffect(() => {
if (events !== null) {
// This uses https://www.npmjs.com/package/canvas-confetti
if ("confetti" in window && typeof window.confetti !== "undefined") {
const duration = 3.5 * 1000;
const animationEnd = Date.now() + duration;
const defaults = {
startVelocity: 30,
spread: 360,
ticks: 60,
zIndex: 0,
colors: [
"#E7FF52",
"#41FF54",
"rgb(245 158 11)",
"rgb(22 163 74)",
"rgb(37 99 235)",
"rgb(67 56 202)",
"rgb(219 39 119)",
"rgb(225 29 72)",
"rgb(217 70 239)",
],
};
function randomInRange(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
// @ts-ignore
const interval = setInterval(function () {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
const particleCount = 50 * (timeLeft / duration);
// since particles fall down, start a bit higher than random
// @ts-ignore
window.confetti(
Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.1, 0.4), y: Math.random() - 0.2 },
})
);
// @ts-ignore
window.confetti(
Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.6, 0.9), y: Math.random() - 0.2 },
})
);
}, 250);
}
revalidator.revalidate();
}
// WARNING Don't put the revalidator in the useEffect deps array or bad things will happen
}, [events]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div
className="-ml-4 -mt-4 h-full w-[calc(100%+32px)] bg-cover bg-no-repeat pt-20"
style={{ backgroundImage: `url("${gradientBackground}")` }}
>
<div className="mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
</Header1>
<Feedback
button={
<Button variant="secondary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
<RadioGroup
className="mb-4 flex gap-x-2"
onValueChange={(value) => setSelectedValue(value as SelectionChoices)}
>
Refresh
</Button>
</StepContentContainer>
</>
<RadioGroupItem
label="Use an existing Next.js project"
description="Use Trigger.dev in an existing Next.js project in less than 2 mins."
value="use-existing-project"
checked={selectedValue === "use-existing-project"}
variant="icon"
data-action="use-existing-project"
icon={<NamedIcon className="h-12 w-12 text-green-600" name={"tree"} />}
/>
<RadioGroupItem
label="Create a new Next.js project"
description="This is the quickest way to try out Trigger.dev in a new Next.js project and takes 5 mins."
value="create-new-next-app"
checked={selectedValue === "create-new-next-app"}
variant="icon"
data-action="create-new-next-app"
icon={<NamedIcon className="h-8 w-8 text-green-600" name={"sapling"} />}
/>
</RadioGroup>
{selectedValue && (
<>
{selectedValue === "create-new-next-app" ? (
<>
<StepNumber stepNumber="1" title="Create a new Next.js project" />
<StepContentContainer>
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`npx create-next-app@latest`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm create next-app`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn create next-app`}
/>
</ClientTabsContent>
</ClientTabs>
<Paragraph spacing variant="small">
Trigger.dev works with either the Pages or App Router configuration.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Navigate to your new Next.js project" />
<StepContentContainer>
<Paragraph spacing>
You have now created a new Next.js project. Lets <InlineCode>cd</InlineCode>{" "}
into it using the project name you just provided:
</Paragraph>
<ClipboardField
value={"cd [replace with your project name]"}
variant={"primary/medium"}
></ClipboardField>
</StepContentContainer>
<StepNumber
stepNumber="3"
title="Run the CLI 'init' command in your new Next.js project"
/>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very
simple example Job in <InlineCode variant="extra-small">examples.ts</InlineCode>{" "}
to help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="4" title="Run your Next.js app" />
<StepContentContainer>
<NextDevCommand />
</StepContentContainer>
<StepNumber stepNumber="5" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</>
) : (
<>
<StepNumber
stepNumber="1"
title="Run the CLI 'init' command in an existing Next.js project"
/>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very
simple example Job in <InlineCode variant="extra-small">examples.ts</InlineCode>{" "}
to help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Next.js app" />
<StepContentContainer>
<NextDevCommand />
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="4" title="Wait for Jobs" displaySpinner />
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</>
)}
</>
)}
</div>
</div>
);
}
@@ -302,6 +414,60 @@ export function HowToUseThisIntegration({ integration, help, integrationClient }
);
}
export function HowToDisableAJob({
id,
name,
version,
}: {
id: string;
name: string;
version: string;
}) {
return (
<>
<Paragraph spacing>
To disable a job, you need to set the <InlineCode>enabled</InlineCode> property to{" "}
<InlineCode>false</InlineCode>.
</Paragraph>
<StepNumber
stepNumber="1"
title={
<>
Set <InlineCode>enabled</InlineCode> to <InlineCode>false</InlineCode>
</>
}
/>
<StepContentContainer>
<CodeBlock
showLineNumbers={false}
className="mb-4"
code={`client.defineJob({
id: "${id}",
name: "${name}",
version: "${version}",
enabled: false,
// ...rest of your Job definition
});`}
/>
</StepContentContainer>
<StepNumber
stepNumber="2"
title={
<>
Run the <InlineCode>@trigger.dev/cli dev</InlineCode> command
</>
}
/>
<StepContentContainer>
<Paragraph spacing>
If you aren't already running the <InlineCode>dev</InlineCode> command, run it now.
</Paragraph>
<TriggerDevCommand />
</StepContentContainer>
</>
);
}
export function HowToUseApiKeysAndEndpoints() {
return (
<>
@@ -391,3 +557,112 @@ export function HowToUseApiKeysAndEndpoints() {
function StepContentContainer({ children }: { children: React.ReactNode }) {
return <div className="mb-6 ml-9 mt-1">{children}</div>;
}
function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey: string }) {
return (
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`npx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`npx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`pnpm dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`pnpm dlx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`yarn dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`yarn dlx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
function NextDevCommand() {
return (
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`npm run dev`} />
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`pnpm run dev`} />
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`yarn run dev`} />
</ClientTabsContent>
</ClientTabs>
);
}
function TriggerDevCommand() {
return (
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`npx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
function TriggerDevStep() {
return (
<>
<Paragraph spacing>
In a <span className="text-amber-400">separate terminal window or tab</span> run:
</Paragraph>
<TriggerDevCommand />
<Paragraph spacing variant="small">
If youre not running on port 3000 you can specify the port by adding{" "}
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
</Paragraph>
<Paragraph spacing variant="small">
You should leave the <InlineCode variant="extra-small">dev</InlineCode> command running when
you're developing.
</Paragraph>
</>
);
}
@@ -51,11 +51,6 @@ export function NoIntegrationSheet({
)}
</SheetHeader>
<SheetBody>
<Callout variant="info">
We dont have an Integration for the {api.name} API yet but you can request one by
clicking the button above. In the meantime, connect to {api.name} using one of the
methods below.
</Callout>
<CustomHelp name={api.name} />
</SheetBody>
</SheetContent>
@@ -0,0 +1,109 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { cn } from "~/utils/cn";
import { JobStatusTable } from "../JobsStatusTable";
import { Button } from "../primitives/Buttons";
import { Header1, Header2 } from "../primitives/Headers";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import { TextLink } from "../primitives/TextLink";
import { useFetcher } from "@remix-run/react";
import { Spinner } from "../primitives/Spinner";
type JobEnvironment = {
type: RuntimeEnvironmentType;
lastRun?: Date;
version: string;
enabled: boolean;
};
type DeleteJobDialogContentProps = {
id: string;
title: string;
slug: string;
environments: JobEnvironment[];
redirectTo?: string;
};
export function DeleteJobDialogContent({
title,
slug,
environments,
id,
redirectTo,
}: DeleteJobDialogContentProps) {
const canDelete = environments.every((environment) => !environment.enabled);
const fetcher = useFetcher();
const isLoading =
fetcher.state === "submitting" ||
(fetcher.state === "loading" && fetcher.formMethod === "DELETE");
return (
<div className="flex w-full flex-col items-center gap-y-6">
<div className="flex flex-col items-center justify-center gap-y-2">
<Header1>{title}</Header1>
<Paragraph variant="small">ID: {slug}</Paragraph>
</div>
<JobStatusTable environments={environments} />
<Header2
className={cn(
canDelete ? "border-rose-500 bg-rose-500/10" : "border-amber-500 bg-amber-500/10",
"rounded border px-3.5 py-2 text-center text-bright"
)}
>
{canDelete
? "Are you sure you want to delete this Job?"
: "You can't delete this Job until all env are disabled"}
</Header2>
<Paragraph variant="small" className="px-6 text-center">
{canDelete ? (
<>
This will permanently delete the Job <span className="strong text-bright">{title}</span>
. This includes the deletion of all Run history. This cannot be undone.
</>
) : (
<>
This Job is still active in an environment. You need to disable it in your Job code
first before it can be deleted.{" "}
<TextLink to="https://trigger.dev/docs/documentation/guides/jobs/managing#disabling-jobs">
Learn how to disable a Job
</TextLink>
.
</>
)}
</Paragraph>
{canDelete ? (
<fetcher.Form
method="delete"
action={`/resources/jobs/${id}${redirectTo ? `?redirectTo=${redirectTo}` : ""}`}
className="w-full"
>
<Button variant="danger/large" fullWidth>
{isLoading ? (
<Spinner color="white" />
) : (
<>
<NamedIcon
name="trash-can"
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
/>
Delete this Job
</>
)}
</Button>
</fetcher.Form>
) : (
<Button variant="danger/large" fullWidth disabled>
<>
<NamedIcon
name="trash-can"
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
/>
Delete this Job
</>
</Button>
)}
</div>
);
}
@@ -0,0 +1,29 @@
import { ActiveBadge, MissingIntegrationBadge, NewBadge } from "../ActiveBadge";
type JobStatusBadgeProps = {
enabled: boolean;
hasIntegrationsRequiringAction: boolean;
hasRuns: boolean;
badgeSize?: "small" | "normal";
};
export function JobStatusBadge({
enabled,
hasIntegrationsRequiringAction,
hasRuns,
badgeSize = "normal",
}: JobStatusBadgeProps) {
if (!enabled) {
return <ActiveBadge active={false} badgeSize={badgeSize} />;
}
if (hasIntegrationsRequiringAction) {
return <MissingIntegrationBadge badgeSize={badgeSize} />;
}
if (!hasRuns) {
return <NewBadge badgeSize={badgeSize} />;
}
return <ActiveBadge active={true} badgeSize={badgeSize} />;
}
+46 -12
View File
@@ -1,25 +1,29 @@
import { jobPath } from "~/utils/pathBuilder";
import { ProjectJob } from "~/hooks/useJobs";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { JobRunStatus } from "~/models/job.server";
import { jobPath, jobTestPath } from "~/utils/pathBuilder";
import { Button } from "../primitives/Buttons";
import { DateTime } from "../primitives/DateTime";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
import { LabelValueStack } from "../primitives/LabelValueStack";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import { PopoverMenuItem } from "../primitives/Popover";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellChevron,
TableCellMenu,
TableHeader,
TableHeaderCell,
TableRow,
} from "../primitives/Table";
import { SimpleTooltip } from "../primitives/Tooltip";
import { runStatusTitle } from "../runs/RunStatuses";
import { ProjectJob } from "~/hooks/useJobs";
import { useProject } from "~/hooks/useProject";
import { useOrganization } from "~/hooks/useOrganizations";
import { JobRunStatus } from "~/models/job.server";
import { cn } from "~/utils/cn";
import { DeleteJobDialogContent } from "./DeleteJobModalContent";
import { JobStatusBadge } from "./JobStatusBadge";
export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) {
const organization = useOrganization();
@@ -34,6 +38,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
<TableHeaderCell>Integrations</TableHeaderCell>
<TableHeaderCell>Properties</TableHeaderCell>
<TableHeaderCell>Last run</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
</TableRow>
</TableHeader>
@@ -42,10 +47,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
jobs.map((job) => {
const path = jobPath(organization, project, job);
return (
<TableRow
key={job.id}
className={cn(job.hasIntegrationsRequiringAction && "bg-rose-500/30")}
>
<TableRow key={job.id} className="group">
<TableCell to={path}>
<span className="flex items-center gap-2">
<NamedIcon name={job.event.icon} className="h-8 w-8" />
@@ -141,7 +143,39 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
<LabelValueStack label={"Never run"} value={""} />
)}
</TableCell>
<TableCellChevron to={path} />
<TableCell to={path}>
<JobStatusBadge
enabled={job.status === "ACTIVE"}
hasIntegrationsRequiringAction={job.hasIntegrationsRequiringAction}
hasRuns={job.lastRun !== undefined}
/>
</TableCell>
<TableCellMenu isSticky>
<PopoverMenuItem to={path} title="View Job" icon="eye" />
<PopoverMenuItem
to={jobTestPath(organization, project, job)}
title="Test Job"
icon="beaker"
/>
<Dialog>
<DialogTrigger asChild>
<Button variant="menu-item" LeadingIcon="trash-can">
Delete Job
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DeleteJobDialogContent
id={job.id}
title={job.title}
slug={job.slug}
environments={job.environments}
/>
</DialogHeader>
</DialogContent>
</Dialog>
</TableCellMenu>
</TableRow>
);
})
@@ -11,7 +11,7 @@ import { docsRoot } from "~/utils/pathBuilder";
export function NavBar() {
return (
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-divide py-1 pl-1 pr-2.5">
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-uiBorder py-1 pl-1 pr-2.5">
<div className="flex gap-0.5">
<Link to="/" className="p-2">
<LogoIcon className="h-5 w-5" />
@@ -58,7 +58,7 @@ export function ProjectSideMenu() {
variants={menuVariants}
initial={isCollapsed ? "collapsed" : "expanded"}
className={cn(
"flex h-full flex-col justify-between overflow-hidden border-r border-slate-850 p-1 transition duration-300 ease-in-out"
"flex h-full flex-col justify-between overflow-hidden border-r border-uiBorder p-1 transition duration-300 ease-in-out"
)}
>
<div className="flex flex-col gap-1">
@@ -12,6 +12,10 @@ type BadgeProps = React.HTMLAttributes<HTMLDivElement> & {
variant?: keyof typeof variants;
};
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
return <div className={cn(variants[variant], className)} {...props} />;
export function Badge({ className, variant = "default", children, ...props }: BadgeProps) {
return (
<div className={cn(variants[variant], className)} {...props}>
<span className="-mb-0.5">{children}</span>
</div>
);
}
@@ -87,6 +87,14 @@ const variant = {
shortcutVariant: undefined,
shortcut: undefined,
},
"danger/large": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50",
button:
"h-10 px-2 text-md bg-rose-600 group-hover:bg-rose-500 group-disabled:opacity-50 group-disabled:group-hover:bg-rose-600",
icon: "h-5",
shortcutVariant: "medium" as const,
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"menu-item": {
textColor: "text-bright",
button:
@@ -2,8 +2,9 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "~/utils/cn";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { ShortcutKey } from "./ShortcutKey";
const Dialog = DialogPrimitive.Root;
@@ -25,7 +26,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100",
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
className
)}
{...props}
@@ -42,15 +43,29 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in sm:max-w-lg sm:rounded-lg sm:zoom-in-90",
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-midnight-800 p-6 pt-11 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
className
)}
{...props}
>
<hr className="-ml-6 w-[calc(100%_+_3rem)]" />
{children}
<DialogPrimitive.Close className="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
<DialogPrimitive.Close className="absolute right-3 top-3 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none">
<div className="flex gap-x-2">
<ShortcutKey
shortcut={{
windows: {
key: "esc",
},
mac: {
key: "esc",
},
}}
variant={"small"}
/>
<XMarkIcon className="h-5 w-5" />
<span className="sr-only">Close</span>
</div>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
@@ -21,7 +21,7 @@ export function FormTitle({
<div
className={cn(
"mb-4 flex flex-col gap-3 pb-4",
divide ? "border-b border-slate-800" : "",
divide ? "border-b border-uiBorder" : "",
className
)}
>
@@ -1,5 +1,5 @@
import { cn } from "~/utils/cn";
import { Paragraph, ParagraphVariant } from "./Paragraph";
import { Paragraph } from "./Paragraph";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
import { SimpleTooltip } from "./Tooltip";
@@ -27,6 +27,7 @@ import {
Cog8ToothIcon,
CreditCardIcon,
EnvelopeIcon,
EyeIcon,
FingerPrintIcon,
FlagIcon,
FolderIcon,
@@ -41,6 +42,7 @@ import {
QrCodeIcon,
SquaresPlusIcon,
StarIcon,
TrashIcon,
UserCircleIcon,
UserGroupIcon,
UserIcon,
@@ -50,7 +52,7 @@ import {
XMarkIcon,
} from "@heroicons/react/24/solid";
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
import { HourglassIcon } from "lucide-react";
import { ActivityIcon, HourglassIcon } from "lucide-react";
import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon";
import { ErrorIcon } from "~/assets/icons/ErrorIcon";
import { ScheduleIcon } from "~/assets/icons/ScheduleIcon";
@@ -58,6 +60,9 @@ import { WebhookIcon } from "~/assets/icons/WebhookIcon";
import { cn } from "~/utils/cn";
import { LogoIcon } from "../LogoIcon";
import { Spinner } from "./Spinner";
import { SaplingIcon } from "~/assets/icons/SaplingIcon";
import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon";
import { OneTreeIcon } from "~/assets/icons/OneTreeIcon";
const icons = {
account: (className: string) => <UserCircleIcon className={cn("text-slate-400", className)} />,
@@ -97,6 +102,7 @@ const icons = {
folder: (className: string) => <FolderIcon className={cn("text-indigo-600", className)} />,
envelope: (className: string) => <EnvelopeIcon className={cn("text-cyan-500", className)} />,
environment: (className: string) => <KeyIcon className={cn("text-yellow-500", className)} />,
eye: (className: string) => <EyeIcon className={cn("text-blue-500", className)} />,
globe: (className: string) => <GlobeAltIcon className={cn("text-fuchsia-600", className)} />,
"hand-raised": (className: string) => (
<HandRaisedIcon className={cn("text-amber-400", className)} />
@@ -126,8 +132,10 @@ const icons = {
<PlusSmallIcon className={cn("text-green-600", className)} />
),
property: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
pulse: (className: string) => <ActivityIcon className={cn("text-green-600", className)} />,
"qr-code": (className: string) => <QrCodeIcon className={cn("text-amber-400", className)} />,
refresh: (className: string) => <ArrowPathIcon className={cn("text-bright", className)} />,
sapling: (className: string) => <SaplingIcon className={cn("text-green-500", className)} />,
search: (className: string) => <MagnifyingGlassIcon className={cn("text-dimmed", className)} />,
settings: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
spinner: (className: string) => <Spinner className={className} color="blue" />,
@@ -135,6 +143,9 @@ const icons = {
star: (className: string) => <StarIcon className={cn("text-yellow-500", className)} />,
stop: (className: string) => <StopIcon className={cn("text-rose-500", className)} />,
team: (className: string) => <UserGroupIcon className={cn("text-blue-500", className)} />,
"trash-can": (className: string) => <TrashIcon className={cn("text-rose-500", className)} />,
tree: (className: string) => <OneTreeIcon className={cn("text-green-500", className)} />,
trees: (className: string) => <TwoTreesIcon className={cn("text-green-500", className)} />,
trigger: (className: string) => <BoltIcon className={cn("text-orange-500", className)} />,
user: (className: string) => <UserIcon className={cn("text-blue-600", className)} />,
warning: (className: string) => (
@@ -13,7 +13,7 @@ type WithChildren = {
export function PageHeader({ children, hideBorder }: WithChildren & { hideBorder?: boolean }) {
return (
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-slate-800 pb-4")}>
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-uiBorder pb-4")}>
{children}
</div>
);
@@ -4,9 +4,8 @@ import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import { ChevronDownIcon } from "@heroicons/react/24/solid";
import { ChevronDownIcon, EllipsisVerticalIcon } from "@heroicons/react/24/solid";
import { LinkButton } from "./Buttons";
import { IconNames } from "./NamedIcon";
const Popover = PopoverPrimitive.Root;
@@ -91,6 +90,24 @@ function PopoverArrowTrigger({
);
}
function PopoverVerticalEllipseTrigger({
isOpen,
className,
...props
}: { isOpen?: boolean } & React.ComponentPropsWithoutRef<typeof PopoverTrigger>) {
return (
<PopoverTrigger
{...props}
className={cn(
"group flex items-center justify-end gap-1 rounded px-1.5 py-1.5 text-dimmed transition hover:bg-slate-750 hover:text-bright",
className
)}
>
<EllipsisVerticalIcon className={cn("h-5 w-5 transition group-hover:text-bright")} />
</PopoverTrigger>
);
}
export {
Popover,
PopoverTrigger,
@@ -98,4 +115,5 @@ export {
PopoverSectionHeader,
PopoverArrowTrigger,
PopoverMenuItem,
PopoverVerticalEllipseTrigger,
};
@@ -11,12 +11,14 @@ const variants = {
label: "text-sm text-bright mt-0.5 select-none",
description: "text-dimmed",
inputPosition: "mt-1",
icon: "w-8 h-8 mb-2",
},
simple: {
button: "w-fit pr-4 data-[disabled]:opacity-70",
label: "text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-1",
icon: "w-8 h-8 mb-2",
},
"button/small": {
button:
@@ -24,6 +26,7 @@ const variants = {
label: "text-sm text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-0",
icon: "w-8 h-8 mb-2",
},
button: {
button:
@@ -31,6 +34,7 @@ const variants = {
label: "text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-1",
icon: "w-8 h-8 mb-2",
},
description: {
button:
@@ -38,6 +42,15 @@ const variants = {
label: "text-bright font-semibold -mt-1 text-left",
description: "text-dimmed -mt-0 text-left",
inputPosition: "mt-0",
icon: "w-8 h-8 mb-2",
},
icon: {
button:
"w-full p-2.5 pb-4 hover:bg-slate-850 transition data-[disabled]:opacity-70 data-[state=checked]:bg-slate-850 border-slate-800 border rounded-sm",
label: "text-bright font-semibold -mt-1 text-left",
description: "text-dimmed -mt-0 text-left",
inputPosition: "mt-0",
icon: "mb-3",
},
};
@@ -57,53 +70,60 @@ type RadioGroupItemProps = Omit<
description?: string;
badges?: string[];
className?: string;
icon?: React.ReactNode;
};
export const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
RadioGroupItemProps
>(({ className, children, variant = "simple", label, description, badges, ...props }, ref) => {
const variation = variants[variant];
>(
(
{ className, children, variant = "simple", label, description, badges, icon, ...props },
ref
) => {
const variation = variants[variant];
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"group flex cursor-pointer items-start gap-x-2 transition",
variation.button,
className
)}
{...props}
>
<div
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-sm border border-slate-700 ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
variation.inputPosition
"group flex cursor-pointer items-start gap-x-2 transition",
variation.button,
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex h-full w-full items-center justify-center bg-indigo-700">
<Circle className="h-1.5 w-1.5 fill-white text-white" />
</RadioGroupPrimitive.Indicator>
</div>
<div>
<div className="flex items-center gap-x-2">
<label htmlFor={props.id} className={cn("cursor-pointer", variation.label)}>
{label}
</label>
{badges && (
<span className="-mr-2 flex gap-x-1.5">
{badges.map((badge) => (
<Badge key={badge}>{badge}</Badge>
))}
</span>
<div
className={cn(
"aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-sm border border-slate-700 ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
variation.inputPosition
)}
>
<RadioGroupPrimitive.Indicator className="flex h-full w-full items-center justify-center bg-indigo-700">
<Circle className="h-1.5 w-1.5 fill-white text-white" />
</RadioGroupPrimitive.Indicator>
</div>
<div className={cn(icon ? "flex h-full flex-col justify-end" : "")}>
{variant === "icon" && <div className={variation.icon}>{icon}</div>}
<div className="flex items-center gap-x-2">
<label htmlFor={props.id} className={cn("cursor-pointer", variation.label)}>
{label}
</label>
{badges && (
<span className="-mr-2 flex gap-x-1.5">
{badges.map((badge) => (
<Badge key={badge}>{badge}</Badge>
))}
</span>
)}
</div>
{(variant === "description" || variant === "icon") && (
<Paragraph variant="small" className={cn("mt-0.5", variation.description)}>
{description}
</Paragraph>
)}
</div>
{variant === "description" && (
<Paragraph variant="small" className={cn("mt-0.5", variation.description)}>
{description}
</Paragraph>
)}
</div>
</RadioGroupPrimitive.Item>
);
});
</RadioGroupPrimitive.Item>
);
}
);
@@ -51,7 +51,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-indigo-900 opacity-100 border-l border-y border-slate-800",
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-uiBorder",
{
variants: {
position: {
@@ -154,7 +154,7 @@ const SheetContent = React.forwardRef<
{...props}
>
<div className="grid max-h-full grid-rows-[2.75rem_1fr] overflow-hidden">
<div className="flex items-center gap-2 border-b border-slate-800 p-2">
<div className="flex items-center gap-2 border-b border-uiBorder p-2">
<SheetPrimitive.Close className="rounded-sm p-1 transition hover:bg-slate-800 disabled:pointer-events-none">
<NamedIcon name="close" className="h-4 w-4" />
<span className="sr-only">Close</span>
@@ -181,7 +181,7 @@ export const SheetBody = ({ className, ...props }: React.HTMLAttributes<HTMLDivE
export const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"mx-4 flex shrink-0 items-center gap-4 border-b border-slate-800 py-3.5",
"mx-4 flex shrink-0 items-center gap-4 border-b border-uiBorder py-3.5",
className
)}
{...props}
@@ -194,7 +194,7 @@ export const SheetFooter = ({
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("shrink-0", className)} {...props}>
<div className="mx-4 border-t border-slate-800 py-3">{children}</div>
<div className="mx-4 border-t border-uiBorder py-3">{children}</div>
</div>
);
@@ -1,10 +1,12 @@
import { cn } from "~/utils/cn";
import { Header2 } from "./Headers";
import { Spinner } from "./Spinner";
export function StepNumber({
stepNumber,
active = false,
complete = false,
displaySpinner = false,
title,
className,
}: {
@@ -13,6 +15,7 @@ export function StepNumber({
complete?: boolean;
title?: React.ReactNode;
className?: string;
displaySpinner?: boolean;
}) {
return (
<div className={cn("mr-3", className)}>
@@ -28,7 +31,15 @@ export function StepNumber({
<span className="flex h-6 w-6 items-center justify-center rounded border border-slate-700 bg-slate-800 py-1 text-xs font-semibold text-dimmed shadow">
{complete ? "✓" : stepNumber}
</span>
<Header2>{title}</Header2>
{displaySpinner ? (
<div className="flex items-center gap-x-2">
<Header2>{title}</Header2>
<Spinner />
</div>
) : (
<Header2>{title}</Header2>
)}
</div>
)}
</div>
+67 -17
View File
@@ -1,24 +1,27 @@
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { ReactNode, forwardRef } from "react";
import { ReactNode, forwardRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Popover, PopoverContent, PopoverVerticalEllipseTrigger } from "./Popover";
type TableProps = {
containerClassName?: string;
className?: string;
children: ReactNode;
fullWidth?: boolean;
};
export const Table = forwardRef<HTMLTableElement, TableProps>(
({ className, containerClassName, children }, ref) => {
({ className, containerClassName, children, fullWidth }, ref) => {
return (
<div
className={cn(
"overflow-x-auto whitespace-nowrap rounded-md border border-slate-900 scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
containerClassName
"overflow-x-auto whitespace-nowrap rounded-md border border-uiBorder scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
containerClassName,
fullWidth && "w-full"
)}
>
<table ref={ref} className={cn("w-full divide-y bg-midnight-850", className)}>
<table ref={ref} className={cn("w-full divide-y", className)}>
{children}
</table>
</div>
@@ -36,7 +39,7 @@ export const TableHeader = forwardRef<HTMLTableSectionElement, TableHeaderProps>
return (
<thead
ref={ref}
className={cn("rounded-t-md", "relative divide-y divide-slate-850", className)}
className={cn("rounded-t-md", "relative divide-y divide-uiBorder bg-slate-850", className)}
>
{children}
</thead>
@@ -52,7 +55,7 @@ type TableBodyProps = {
export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
({ className, children }, ref) => {
return (
<tbody ref={ref} className={cn("relative divide-y divide-slate-850", className)}>
<tbody ref={ref} className={cn("relative divide-y divide-uiBorder", className)}>
{children}
</tbody>
);
@@ -62,12 +65,13 @@ export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
type TableRowProps = {
className?: string;
children: ReactNode;
disabled?: boolean;
};
export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
({ className, children }, ref) => {
({ className, disabled, children }, ref) => {
return (
<tr ref={ref} className={cn("group w-full", className)}>
<tr ref={ref} className={cn(disabled && "opacity-50", "group w-full", className)}>
{children}
</tr>
);
@@ -102,7 +106,7 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
ref={ref}
scope="col"
className={cn(
"px-4 py-3 align-middle text-xs font-semibold uppercase text-slate-400",
"px-4 py-3 align-middle text-xs font-normal uppercase tracking-wider text-dimmed",
alignmentClassName,
className
)}
@@ -117,10 +121,11 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
type TableCellProps = TableCellBasicProps & {
to?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
hasAction?: boolean;
};
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
({ className, alignment = "left", children, colSpan, to, onClick }, ref) => {
({ className, alignment = "left", children, colSpan, to, onClick, hasAction = false }, ref) => {
let alignmentClassName = "text-left";
switch (alignment) {
case "center":
@@ -132,7 +137,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
}
const flexClasses = cn(
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-slate-400",
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-dimmed",
alignment === "left"
? "justify-start text-left"
: alignment === "center"
@@ -144,8 +149,10 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
<td
ref={ref}
className={cn(
"text-xs text-slate-400 transition group-hover:bg-slate-850/50",
to || onClick ? "cursor-pointer" : "px-4 py-3 align-middle",
"text-xs text-slate-400",
to || onClick || hasAction
? "cursor-pointer group-hover:bg-slate-900"
: "px-4 py-3 align-middle",
!to && !onClick && alignmentClassName,
className
)}
@@ -167,17 +174,60 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
}
);
const stickyStyles =
"sticky right-0 z-10 w-[2.8rem] min-w-[2.8rem] bg-background before:absolute before:pointer-events-none before:-left-8 before:top-0 before:h-full before:min-w-[2rem] before:bg-gradient-to-r before:from-transparent before:to-background before:content-[''] group-hover:before:to-slate-900";
export const TableCellChevron = forwardRef<
HTMLTableCellElement,
{
className?: string;
to?: string;
children?: ReactNode;
isSticky?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
>(({ className, to, onClick }, ref) => {
>(({ className, to, children, isSticky, onClick }, ref) => {
return (
<TableCell className={className} to={to} onClick={onClick} ref={ref} alignment="right">
<ChevronRightIcon className="h-4 w-4 text-slate-700 transition group-hover:text-bright" />
<TableCell
className={cn(isSticky && stickyStyles, className)}
to={to}
onClick={onClick}
ref={ref}
alignment="right"
>
{children}
<ChevronRightIcon className="h-4 w-4 text-dimmed transition group-hover:text-bright" />
</TableCell>
);
});
export const TableCellMenu = forwardRef<
HTMLTableCellElement,
{
className?: string;
children?: ReactNode;
isSticky?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
>(({ className, children, isSticky, onClick }, ref) => {
const [isOpen, setIsOpen] = useState(false);
return (
<TableCell
className={cn(isSticky && stickyStyles, className)}
onClick={onClick}
ref={ref}
alignment="right"
hasAction={true}
>
<Popover onOpenChange={(open) => setIsOpen(open)}>
<PopoverVerticalEllipseTrigger isOpen={isOpen} />
<PopoverContent
className="w-fit max-w-[10rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
align="end"
>
<div className="flex flex-col gap-1 p-1">{children}</div>
</PopoverContent>
</Popover>
</TableCell>
);
});
@@ -12,7 +12,7 @@ export type TabsProps = {
export function Tabs({ tabs, className }: TabsProps) {
return (
<div className={cn(`flex flex-row gap-x-6 border-b border-slate-800`, className)}>
<div className={cn(`flex flex-row gap-x-6 border-b border-uiBorder`, className)}>
{tabs.map((tab, index) => (
<NavLink key={index} to={tab.to} className="group flex flex-col items-center pt-1" end>
{({ isActive, isPending }) => (
@@ -273,6 +273,7 @@ function BlankTasks({
basicStatus: RunBasicStatus;
}) {
switch (basicStatus) {
default:
case "COMPLETED":
return <Paragraph variant="small">There were no tasks for this run.</Paragraph>;
case "FAILED":
@@ -288,8 +289,6 @@ function BlankTasks({
<TaskCardSkeleton />
</div>
);
default:
return <Paragraph variant="small">There were no tasks for this run.</Paragraph>;
}
}
@@ -325,7 +324,7 @@ function RerunPopover({
<Form method="post" action={`/resources/runs/${runId}/rerun`} {...form.props}>
<input {...conform.input(successRedirect, { type: "hidden" })} defaultValue={runsPath} />
{environmentType === "PRODUCTION" && (
<Callout variant="warning">
<Callout variant="warning" className="mb-2">
This will rerun this Job in your Production environment.
</Callout>
)}
@@ -109,7 +109,7 @@ export function RunsTable({
<TableCell to={path}>
{run.createdAt ? <DateTime date={run.createdAt} /> : ""}
</TableCell>
<TableCellChevron to={path} />
<TableCellChevron to={path} isSticky />
</TableRow>
);
})
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from "@storybook/react";
import { withDesign } from "storybook-addon-designs";
import { Badge } from "../primitives/Badge";
const meta: Meta = {
title: "Primitives/Badges",
decorators: [withDesign],
};
export default meta;
type Story = StoryObj<typeof BadgesExample>;
export const Basic: Story = {
render: () => <BadgesExample />,
};
function BadgesExample() {
return (
<div className="flex flex-col items-start gap-y-8 p-8">
<Badge>Default</Badge>
<Badge variant="outline">Outline</Badge>
</div>
);
}
@@ -253,13 +253,11 @@ function ButtonList({ primary }: { primary: string }) {
<Header1 className="mb-2 mt-8">Large buttons</Header1>
<div className="grid grid-cols-1 gap-8 border-b border-slate-700 pb-8">
<div className="flex flex-col gap-2">
<div className="flex flex-col items-start">
<div className="flex flex-col items-start gap-2">
<Button variant="primary/large" fullWidth>
<NamedIcon name={"github"} className={"mr-1.5 h-4 w-4"} />
Continue with GitHub
</Button>
</div>
<div className="flex flex-col items-start">
<Button variant="secondary/large" fullWidth>
<NamedIcon
name={"envelope"}
@@ -267,6 +265,13 @@ function ButtonList({ primary }: { primary: string }) {
/>
Continue with Email
</Button>
<Button variant="danger/large" fullWidth>
<NamedIcon
name={"trash-can"}
className={"mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"}
/>
This is a delete button
</Button>
</div>
</div>
</div>
@@ -3,6 +3,7 @@ import { withDesign } from "storybook-addon-designs";
import { useState } from "react";
import { Button } from "../primitives/Buttons";
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
import { NamedIcon } from "../primitives/NamedIcon";
const meta: Meta = {
title: "Primitives/RadioGroup",
@@ -42,6 +43,14 @@ function RadioGroupExample() {
value={"5"}
variant="description"
/>
<RadioGroupItem
id="r7"
label="This is an icon label"
description="This is a description"
value={"6"}
variant="icon"
icon={<NamedIcon name="tree" className="h-8 w-8" />}
/>
</RadioGroup>
</form>
</div>
+2 -1
View File
@@ -1,6 +1,7 @@
export const LIVE_ENVIRONMENT = "live";
export const DEV_ENVIRONMENT = "development";
export const MAX_LIVE_PROJECTS = 1;
export const DEFAULT_MAX_CONCURRENT_RUNS = 100;
export const DEFAULT_MAX_CONCURRENT_RUNS = 10;
export const MAX_CONCURRENT_RUNS_LIMIT = 20;
export const PREPROCESS_RETRY_LIMIT = 2;
export const EXECUTE_JOB_RETRY_LIMIT = 10;
+17 -2
View File
@@ -2,6 +2,7 @@ import { PrismaClient, Prisma } from "@trigger.dev/database";
import invariant from "tiny-invariant";
import { z } from "zod";
import { logger } from "./services/logger.server";
import { env } from "./env.server";
export type PrismaTransactionClient = Omit<
PrismaClient,
@@ -29,6 +30,8 @@ export type PrismaTransactionOptions = {
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
isolationLevel?: Prisma.TransactionIsolationLevel;
rethrowPrismaErrors?: boolean;
};
export async function $transaction<R>(
@@ -52,6 +55,10 @@ export async function $transaction<R>(
name: error.name,
});
if (options?.rethrowPrismaErrors) {
throw error;
}
return;
}
@@ -84,8 +91,16 @@ function getClient() {
const { DATABASE_URL } = process.env;
invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set");
const databaseUrl = new URL(DATABASE_URL);
// We need to add the connection_limit and pool_timeout query params to the url, in a way that works if the DATABASE_URL already has query params
const query = databaseUrl.searchParams;
query.set("connection_limit", env.DATABASE_CONNECTION_LIMIT.toString());
query.set("pool_timeout", env.DATABASE_POOL_TIMEOUT.toString());
databaseUrl.search = query.toString();
// Remove the username:password in the url and print that to the console
const urlWithoutCredentials = new URL(DATABASE_URL);
const urlWithoutCredentials = new URL(databaseUrl.href);
urlWithoutCredentials.password = "";
console.log(`🔌 setting up prisma client to ${urlWithoutCredentials.toString()}`);
@@ -93,7 +108,7 @@ function getClient() {
const client = new PrismaClient({
datasources: {
db: {
url: DATABASE_URL,
url: databaseUrl.href,
},
},
log: [
+2 -2
View File
@@ -74,7 +74,7 @@ function serveTheBots(
{
// Use onAllReady to wait for the entire document to be ready
onAllReady() {
responseHeaders.set("Content-Type", "text/html");
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
let body = new PassThrough();
pipe(body);
resolve(
@@ -114,7 +114,7 @@ function serveBrowsers(
// use onShellReady to wait until a suspense boundary is triggered
onShellReady() {
shellReady = true;
responseHeaders.set("Content-Type", "text/html");
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
let body = new PassThrough();
pipe(body);
resolve(
+12
View File
@@ -4,6 +4,9 @@ import { SecretStoreOptionsSchema } from "./services/secrets/secretStore.server"
const EnvironmentSchema = z.object({
NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]),
DATABASE_URL: z.string(),
DATABASE_CONNECTION_LIMIT: z.coerce.number().int().default(10),
DATABASE_POOL_TIMEOUT: z.coerce.number().int().default(60),
DIRECT_URL: z.string(),
SESSION_SECRET: z.string(),
MAGIC_LINK_SECRET: z.string(),
ENCRYPTION_KEY: z.string(),
@@ -20,6 +23,8 @@ const EnvironmentSchema = z.object({
.default(process.env.NODE_ENV),
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
POSTHOG_PROJECT_KEY: z.string().optional(),
TELEMETRY_TRIGGER_API_KEY: z.string().optional(),
TELEMETRY_TRIGGER_API_URL: z.string().optional(),
HIGHLIGHT_PROJECT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
@@ -28,6 +33,13 @@ const EnvironmentSchema = z.object({
RESEND_API_KEY: z.string().optional(),
PLAIN_API_KEY: z.string().optional(),
RUNTIME_PLATFORM: z.enum(["docker-compose", "ecs", "local"]).default("local"),
WORKER_SCHEMA: z.string().default("graphile_worker"),
WORKER_CONCURRENCY: z.coerce.number().int().default(10),
WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
EXECUTION_WORKER_CONCURRENCY: z.coerce.number().int().default(10),
EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
WORKER_ENABLED: z.string().default("true"),
EXECUTION_WORKER_ENABLED: z.string().default("true"),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
+4
View File
@@ -8,6 +8,10 @@ export type ProjectJob = UseDataFunctionReturn<typeof loader>["projectJobs"][num
export const jobsMatchId =
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
// This is only used in the JobsMenu component, which is the breadcrumb job list dropdown.
// This dropdown is only shown once you have selected a job, so we can assume that
// the route above has loaded and we can use the data from it.
export function useOptionalJobs(matches?: RouteMatch[]) {
const routeMatch = useTypedMatchesData<typeof loader>({
id: jobsMatchId,
@@ -0,0 +1,60 @@
import { JobRun, JobRunExecution } from "@trigger.dev/database";
import { PrismaClientOrTransaction } from "~/db.server";
import { executionWorker } from "~/services/worker.server";
export async function enqueueRunExecutionV1(
execution: JobRunExecution,
queueId: string,
concurrency: number,
tx: PrismaClientOrTransaction,
runAt?: Date
) {
const job = await executionWorker.enqueue(
"performRunExecution",
{
id: execution.id,
},
{
queueName: `job:queue:${queueId}`,
tx,
runAt,
jobKey: `execution:${execution.runId}`,
}
);
}
export type EnqueueRunExecutionV2Options = {
runAt?: Date;
resumeTaskId?: string;
isRetry?: boolean;
skipRetrying?: boolean;
};
export async function enqueueRunExecutionV2(
run: JobRun,
tx: PrismaClientOrTransaction,
options: EnqueueRunExecutionV2Options = {}
) {
const job = await executionWorker.enqueue(
"performRunExecutionV2",
{
id: run.id,
reason: run.status === "PREPROCESSING" ? "PREPROCESS" : "EXECUTE_JOB",
resumeTaskId: options.resumeTaskId,
isRetry: typeof options.isRetry === "boolean" ? options.isRetry : false,
},
{
queueName: `job:${run.jobId}:env:${run.environmentId}`,
tx,
runAt: options.runAt,
jobKey: `job_run:${run.id}`,
maxAttempts: options.skipRetrying ? 1 : undefined,
}
);
}
export async function dequeueRunExecutionV2(run: JobRun, tx: PrismaClientOrTransaction) {
return await executionWorker.dequeue(`job_run:${run.id}`, {
tx,
});
}
+19
View File
@@ -102,6 +102,25 @@ export async function jsonWithSuccessMessage(
});
}
export async function jsonWithErrorMessage(
data: any,
request: Request,
message: string,
options?: ToastMessageOptions
) {
const session = await getSession(request.headers.get("cookie"));
setErrorMessage(session, message, options);
return json(data, {
headers: {
"Set-Cookie": await commitSession(session, {
expires: new Date(Date.now() + ONE_YEAR),
}),
},
});
}
export async function redirectWithSuccessMessage(
path: string,
request: Request,
@@ -49,6 +49,7 @@ export function getOrganizations({ userId }: { userId: User["id"] }) {
jobs: {
where: {
internal: false,
deletedAt: null,
},
},
},
+231 -11
View File
@@ -1,4 +1,6 @@
import type {
CronItem,
CronItemOptions,
Job as GraphileJob,
Runner as GraphileRunner,
JobHelpers,
@@ -7,7 +9,7 @@ import type {
TaskList,
TaskSpec,
} from "graphile-worker";
import { run as graphileRun } from "graphile-worker";
import { run as graphileRun, parseCronItems } from "graphile-worker";
import omit from "lodash.omit";
import { z } from "zod";
@@ -18,6 +20,13 @@ export interface MessageCatalogSchema {
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
}
const RawCronPayloadSchema = z.object({
_cron: z.object({
ts: z.coerce.date(),
backfilled: z.boolean(),
}),
});
const GraphileJobSchema = z.object({
id: z.coerce.string(),
queue_name: z.string().nullable(),
@@ -42,6 +51,7 @@ const AddJobResultsSchema = z.array(GraphileJobSchema);
export type ZodTasks<TConsumerSchema extends MessageCatalogSchema> = {
[K in keyof TConsumerSchema]: {
queueName?: string | ((payload: z.infer<TConsumerSchema[K]>) => string);
jobKey?: string | ((payload: z.infer<TConsumerSchema[K]>) => string | undefined);
priority?: number;
maxAttempts?: number;
jobKeyMode?: "replace" | "preserve_run_at" | "unsafe_dedupe";
@@ -50,29 +60,56 @@ export type ZodTasks<TConsumerSchema extends MessageCatalogSchema> = {
};
};
type RecurringTaskPayload = {
ts: Date;
backfilled: boolean;
};
export type ZodRecurringTasks = {
[key: string]: {
pattern: string;
options?: CronItemOptions;
handler: (payload: RecurringTaskPayload, job: GraphileJob) => Promise<void>;
};
};
export type ZodWorkerEnqueueOptions = TaskSpec & {
tx?: PrismaClientOrTransaction;
};
export type ZodWorkerDequeueOptions = {
tx?: PrismaClientOrTransaction;
};
export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
name: string;
runnerOptions: RunnerOptions;
prisma: PrismaClient;
schema: TMessageCatalog;
tasks: ZodTasks<TMessageCatalog>;
recurringTasks?: ZodRecurringTasks;
};
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
#name: string;
#schema: TMessageCatalog;
#prisma: PrismaClient;
#runnerOptions: RunnerOptions;
#tasks: ZodTasks<TMessageCatalog>;
#recurringTasks?: ZodRecurringTasks;
#runner?: GraphileRunner;
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
this.#name = options.name;
this.#schema = options.schema;
this.#prisma = options.prisma;
this.#runnerOptions = options.runnerOptions;
this.#tasks = options.tasks;
this.#recurringTasks = options.recurringTasks;
}
get graphileWorkerSchema() {
return this.#runnerOptions.schema ?? "graphile_worker";
}
public async initialize(): Promise<boolean> {
@@ -80,19 +117,70 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
return true;
}
logger.debug("Initializing worker queue with options", {
this.#logDebug("Initializing worker queue with options", {
runnerOptions: this.#runnerOptions,
});
const parsedCronItems = parseCronItems(this.#createCronItemsFromRecurringTasks());
this.#runner = await graphileRun({
...this.#runnerOptions,
taskList: this.#createTaskListFromTasks(),
parsedCronItems,
});
if (!this.#runner) {
throw new Error("Failed to initialize worker queue");
}
this.#runner?.events.on("pool:create", ({ workerPool }) => {
this.#logDebug("pool:create");
});
this.#runner?.events.on("pool:listen:connecting", ({ workerPool, attempts }) => {
this.#logDebug("pool:create", { attempts });
});
this.#runner?.events.on("pool:listen:success", ({ workerPool, client }) => {
this.#logDebug("pool:listen:success");
});
this.#runner?.events.on("pool:listen:error", ({ error }) => {
this.#logDebug("pool:listen:error", { error });
});
this.#runner?.events.on("pool:gracefulShutdown", ({ message }) => {
this.#logDebug("pool:gracefulShutdown", { workerMessage: message });
});
this.#runner?.events.on("pool:gracefulShutdown:error", ({ error }) => {
this.#logDebug("pool:gracefulShutdown:error", { error });
});
this.#runner?.events.on("worker:create", ({ worker }) => {
this.#logDebug("worker:create", { workerId: worker.workerId });
});
this.#runner?.events.on("worker:release", ({ worker }) => {
this.#logDebug("worker:release", { workerId: worker.workerId });
});
this.#runner?.events.on("worker:stop", ({ worker, error }) => {
this.#logDebug("worker:stop", { workerId: worker.workerId, error });
});
this.#runner?.events.on("worker:fatalError", ({ worker, error, jobError }) => {
this.#logDebug("worker:fatalError", { workerId: worker.workerId, error, jobError });
});
this.#runner?.events.on("gracefulShutdown", ({ signal }) => {
this.#logDebug("gracefulShutdown", { signal });
});
this.#runner?.events.on("stop", () => {
this.#logDebug("stop");
});
return true;
}
@@ -105,23 +193,35 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
payload: z.infer<TMessageCatalog[K]>,
options?: ZodWorkerEnqueueOptions
): Promise<GraphileJob> {
if (!this.#runner) {
throw new Error("Worker not initialized");
}
const task = this.#tasks[identifier];
const optionsWithoutTx = omit(options ?? {}, ["tx"]);
const optionsWithoutTx = removeUndefinedKeys(omit(options ?? {}, ["tx"]));
const taskWithoutJobKey = omit(task, ["jobKey"]);
// Make sure options passed in to enqueue take precedence over task options
const spec = {
...taskWithoutJobKey,
...optionsWithoutTx,
...task,
};
if (typeof task.queueName === "function") {
spec.queueName = task.queueName(payload);
}
if (typeof task.jobKey === "function") {
const jobKey = task.jobKey(payload);
if (jobKey) {
spec.jobKey = jobKey;
}
}
logger.debug("Enqueuing worker task", {
identifier,
payload,
spec,
});
const job = await this.#addJob(
identifier as string,
payload,
@@ -139,6 +239,17 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
return job;
}
public async dequeue(
jobKey: string,
option?: ZodWorkerDequeueOptions
): Promise<GraphileJob | undefined> {
const results = await this.#removeJob(jobKey, option?.tx ?? this.#prisma);
logger.debug("dequeued worker task", { results, jobKey });
return results;
}
async #addJob(
identifier: string,
payload: unknown,
@@ -146,7 +257,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
tx: PrismaClientOrTransaction
) {
const results = await tx.$queryRawUnsafe(
`SELECT * FROM graphile_worker.add_job(
`SELECT * FROM ${this.graphileWorkerSchema}.add_job(
identifier => $1::text,
payload => $2::json,
queue_name => $3::text,
@@ -164,8 +275,8 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
spec.maxAttempts || null,
spec.jobKey || null,
spec.priority || null,
spec.jobKeyMode || null,
spec.flags || null
spec.flags || null,
spec.jobKeyMode || null
);
const rows = AddJobResultsSchema.safeParse(results);
@@ -181,6 +292,32 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
return job as GraphileJob;
}
async #removeJob(jobKey: string, tx: PrismaClientOrTransaction) {
try {
const result = await tx.$queryRawUnsafe(
`SELECT * FROM ${this.graphileWorkerSchema}.remove_job(
job_key => $1::text
)`,
jobKey
);
const job = AddJobResultsSchema.safeParse(result);
if (!job.success) {
logger.debug("results returned from remove_job could not be parsed", {
error: job.error.flatten(),
result,
jobKey,
});
return;
}
return job.data[0] as GraphileJob;
} catch (e) {
throw new Error(`Failed to remove job from queue, ${e}}`);
}
}
#createTaskListFromTasks() {
const taskList: TaskList = {};
@@ -192,9 +329,38 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
taskList[key] = task;
}
for (const [key] of Object.entries(this.#recurringTasks ?? {})) {
const task: Task = (payload, helpers) => {
return this.#handleRecurringTask(key, payload, helpers);
};
taskList[key] = task;
}
return taskList;
}
#createCronItemsFromRecurringTasks() {
const cronItems: CronItem[] = [];
if (!this.#recurringTasks) {
return cronItems;
}
for (const [key, task] of Object.entries(this.#recurringTasks)) {
const cronItem: CronItem = {
pattern: task.pattern,
identifier: key,
task: key,
options: task.options,
};
cronItems.push(cronItem);
}
return cronItems;
}
async #handleMessage<K extends keyof TMessageCatalog>(
typeName: K,
rawPayload: unknown,
@@ -226,4 +392,58 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
await task.handler(payload, job);
}
async #handleRecurringTask(
typeName: string,
rawPayload: unknown,
helpers: JobHelpers
): Promise<void> {
const job = helpers.job;
logger.debug("Received recurring task, calling handler", {
type: String(typeName),
payload: rawPayload,
job,
});
const recurringTask = this.#recurringTasks?.[typeName];
if (!recurringTask) {
throw new Error(`No recurring task for message type: ${String(typeName)}`);
}
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
if (!parsedPayload.success) {
throw new Error(
`Failed to parse recurring task payload: ${JSON.stringify(parsedPayload.error)}`
);
}
const payload = parsedPayload.data;
try {
await recurringTask.handler(payload._cron, job);
} catch (error) {
logger.error("Failed to handle recurring task", {
error,
payload,
});
throw error;
}
}
#logDebug(message: string, args?: any) {
logger.debug(`[worker][${this.#name}] ${message}`, args);
}
}
function removeUndefinedKeys<T extends object>(obj: T): T {
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key] === undefined) {
delete obj[key];
}
}
return obj;
}
@@ -63,6 +63,7 @@ export class IntegrationClientPresenter {
slug: projectSlug,
},
internal: false,
deletedAt: null,
},
},
},
@@ -71,6 +71,7 @@ export class IntegrationsPresenter {
slug: projectSlug,
},
internal: false,
deletedAt: null,
},
},
},
@@ -47,6 +47,7 @@ export class JobListPresenter {
version: true,
eventSpecification: true,
properties: true,
status: true,
runs: {
select: {
createdAt: true,
@@ -92,6 +93,7 @@ export class JobListPresenter {
},
where: {
internal: false,
deletedAt: null,
organization: orgWhere,
project: {
slug: projectSlug,
@@ -162,11 +164,19 @@ export class JobListPresenter {
properties = [...properties, ...versionProperties];
}
const environments = job.aliases.map((alias) => ({
type: alias.environment.type,
enabled: alias.version.status === "ACTIVE",
lastRun: alias.version.runs.at(0)?.createdAt,
version: alias.version.version,
}));
return {
id: job.id,
slug: job.slug,
title: job.title,
version: alias.version.version,
status: alias.version.status,
dynamic: job.dynamicTriggers.length > 0,
event: {
title: eventSpecification.title,
@@ -179,6 +189,7 @@ export class JobListPresenter {
),
lastRun,
properties,
environments,
};
})
.filter(Boolean);
@@ -80,6 +80,7 @@ export class ProjectPresenter {
},
where: {
internal: false,
deletedAt: null,
},
orderBy: [{ title: "asc" }],
},
+2
View File
@@ -16,6 +16,7 @@ import { getUser } from "./services/session.server";
import { appEnvTitleTag } from "./utils";
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
import { useHighlight } from "./hooks/useHighlight";
import { ExternalScripts } from "remix-utils";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
@@ -104,6 +105,7 @@ function App() {
</HighlightErrorBoundary>
<Toast />
<ScrollRestoration />
<ExternalScripts />
<Scripts />
<LiveReload />
</body>
@@ -1,13 +1,17 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { ArrowUpIcon } from "@heroicons/react/24/solid";
import { LoaderArgs, SerializeFrom } from "@remix-run/server-runtime";
import useWindowSize from "react-use/lib/useWindowSize";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import Confetti from "react-confetti";
import { ExternalScriptsFunction } from "remix-utils";
import { HowToSetupYourProject } from "~/components/helpContent/HelpContentText";
import { JobsTable } from "~/components/jobs/JobsTable";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Callout } from "~/components/primitives/Callout";
import { Header2 } from "~/components/primitives/Headers";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Input } from "~/components/primitives/Input";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import {
PageHeader,
PageInfoGroup,
@@ -17,24 +21,19 @@ import {
PageTitleRow,
} from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { TextLink } from "~/components/primitives/TextLink";
import { useFilterJobs } from "~/hooks/useFilterJobs";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { JobListPresenter } from "~/presenters/JobListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import useWindowSize from "react-use/lib/useWindowSize";
import {
docsPath,
ProjectParamSchema,
projectIntegrationsPath,
trimTrailingSlash,
ProjectParamSchema,
} from "~/utils/pathBuilder";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { requireUserId } from "~/services/session.server";
import { JobListPresenter } from "~/presenters/JobListPresenter.server";
import { TextLink } from "~/components/primitives/TextLink";
import { GitHubLightIcon, OpenAILightIcon, ResendIcon } from "@trigger.dev/companyicons";
import { ClockIcon, CalendarDaysIcon, SlackIcon } from "lucide-react";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -59,6 +58,12 @@ export const loader = async ({ request, params }: LoaderArgs) => {
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Jobs" />,
expandSidebar: true,
scripts: (match) => [
{
src: "https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js",
crossOrigin: "anonymous",
},
],
};
export default function Page() {
@@ -68,8 +73,6 @@ export default function Page() {
const { filterText, setFilterText, filteredItems } = useFilterJobs(jobs);
const { width, height } = useWindowSize();
return (
<PageContainer>
<PageHeader>
@@ -83,94 +86,51 @@ export default function Page() {
</PageInfoRow>
</PageHeader>
<PageBody>
{/* Todo: this confetti component needs to trigger when the example project is created, then never again. */}
{/* <Confetti
width={width}
height={height}
recycle={false}
numberOfPieces={1000}
colors={[
"#E7FF52",
"#41FF54",
"rgb(245 158 11)",
"rgb(22 163 74)",
"rgb(37 99 235)",
"rgb(67 56 202)",
"rgb(219 39 119)",
"rgb(225 29 72)",
"rgb(217 70 239)",
]}
/> */}
<Help defaultOpen={jobs.length === 0}>
<Help>
{(open) => (
<div className={cn("grid h-fit gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div>
{jobs.length > 0 && 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">
{jobs.length === 0 ? (
<Header2>Jobs</Header2>
) : (
<Input
placeholder="Search Jobs"
variant="tertiary"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
)}
<HelpTrigger title="How do I add Trigger.dev to my Next.js app?" />
</div>
{jobs.length === 0 ? (
<div>
<div
className={
"flex w-full flex-col justify-center gap-x-4 rounded-md border border-dashed border-indigo-800 px-5 py-8"
}
>
<Paragraph variant="small">Your Jobs will appear here.</Paragraph>
</div>
</div>
) : (
<div className={cn("grid gap-4", open ? "h-full grid-cols-2" : " h-full grid-cols-1")}>
<div className="h-full">
{jobs.length > 0 ? (
<>
{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 flex-col">
<div className="flex w-full">
<Input
placeholder="Search Jobs"
variant="tertiary"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<HelpTrigger title="Example Jobs and inspiration" />
</div>
</div>
<JobsTable
jobs={filteredItems}
noResultsText={`No Jobs match ${filterText}. Try a different search
query.`}
query.`}
/>
{jobs.length === 1 ? (
<>
<Callout
variant="docs"
to={docsPath("documentation/guides/create-a-job")}
className="my-3"
>
Create your first Job in code
</Callout>
<ExampleJobs />
</>
) : (
<Callout
variant="docs"
to={docsPath("documentation/guides/create-a-job")}
className="my-3"
>
Create another Job
</Callout>
)}
{jobs.length === 1 &&
jobs.every((r) => r.lastRun === undefined) &&
jobs.every((i) => i.hasIntegrationsRequiringAction === false) && (
<RunYourJobPrompt />
)}
</>
) : (
<HowToSetupYourProject />
)}
</div>
<HelpContent title="How to add Trigger.dev to your Next.js app (3 mins)">
<HowToSetupYourProject />
<HelpContent title="Example Jobs and inspiration">
<ExampleJobs />
</HelpContent>
</div>
)}
@@ -180,69 +140,155 @@ export default function Page() {
);
}
function ExampleJobs() {
function RunYourJobPrompt() {
return (
<div className="mt-6 flex w-full flex-col gap-y-2 rounded bg-slate-900 p-4">
<Header2 className="text-slate-300">Example Jobs</Header2>
<Paragraph variant="small">
If you want more inspiration or just want to dig into the code of a Job, check out our{" "}
<TextLink href="https://github.com/triggerdotdev/examples">examples repo</TextLink>.
<div className="mt-2 flex w-full gap-x-2 rounded border border-slate-800 bg-slate-900 p-4 pl-6">
<ArrowUpIcon className="h-5 w-5 animate-bounce text-green-500" />
<Paragraph variant="small" className="text-green-500">
Your Job is ready to run! Click it to run it now.
</Paragraph>
<div className="h-[1px] w-full bg-slate-800" />
<div className="flex gap-1.5">
<ClockIcon className="h-4 w-4 pt-0.5 text-slate-100" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/delays">
Delays
</TextLink>{" "}
- Using delays inside Jobs
</Paragraph>
</div>
<div className="flex gap-1.5">
<CalendarDaysIcon className="h-4 w-4 pt-0.5 text-slate-100" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/scheduled">
Scheduled
</TextLink>{" "}
- Interval and cron scheduled Jobs
</Paragraph>
</div>
<div className="flex gap-1.5">
<GitHubLightIcon className="ml-0.5 h-4 w-4 pt-0.5" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/github">
GitHub
</TextLink>{" "}
- When a new GitHub issue is opened it adds a Bug label to it.
</Paragraph>
</div>
<div className="flex gap-1.5">
<OpenAILightIcon className="ml-0.5 h-4 w-4 pt-0.5" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/openai">
OpenAI
</TextLink>{" "}
- Generate images and jokes from a prompt
</Paragraph>
</div>
<div className="flex gap-1.5">
<ResendIcon className="ml-0.5 h-4 w-4 pt-0.5" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/resend">
Resend
</TextLink>{" "}
- Sends an email by submitting a form in the Next.js app
</Paragraph>
</div>{" "}
<div className="flex gap-1.5">
<SlackIcon className="ml-0.5 h-4 w-4 pt-0.5" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/slack">
Slack
</TextLink>{" "}
- Sends a Slack message when an event is received
</Paragraph>
</div>
</div>
);
}
function ExampleJobs() {
return (
<>
<Header2 spacing>Video walk-through</Header2>
<Paragraph spacing variant="small">
Watch Matt, CEO of Trigger.dev create a GitHub issue reminder in Slack using Trigger.dev.
(10 mins)
</Paragraph>
<iframe
src="https://www.youtube.com/embed/uocBQt2HeQo?&showinfo=0&rel=0&modestbranding=1"
title="Trigger.dev explainer video"
width="400"
height="250"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
className="mb-4 w-full border-b border-slate-800"
/>
<Header2 spacing>How to create a Job</Header2>
<Paragraph variant="small" spacing>
Our docs are a great way to learn how to create Jobs with each type of Trigger, from
webhooks, to delays, to triggering Jobs on a schedule.{" "}
</Paragraph>
<a
href="https://trigger.dev/docs/documentation/guides/create-a-job"
className="mb-4 flex w-full items-center rounded border-b border-slate-800 py-2 transition hover:border-transparent hover:bg-slate-800"
>
<NamedIcon name={"external-link"} className={iconStyles} />
<Paragraph variant="small" className="font-semibold text-bright">
How to create a Job
</Paragraph>
</a>
<Header2 spacing>Check out some example Jobs in code</Header2>
<Paragraph spacing variant="small">
If you're looking for inspiration for your next Job, check out our{" "}
<TextLink href="https://github.com/triggerdotdev/examples">examples repo</TextLink>. Or jump
straight into an example repo from the list below:
</Paragraph>
<div className="flex w-full flex-col">
{examples.map((example) => (
<a
href={example.codeLink}
key={example.title}
className="flex w-full items-center rounded border-b border-uiBorder py-2 transition hover:border-transparent hover:bg-slate-800"
>
{example.icon}
<Paragraph variant="small">
<span className="font-semibold text-bright">{example.title}</span> -{" "}
{example.description}
</Paragraph>
</a>
))}
</div>
</>
);
}
const iconStyles = "h-7 w-7 mr-2 pl-2 min-w-[28px]";
const examples = [
{
icon: <NamedIcon name={"clock"} className={iconStyles} />,
title: "Basic delay",
description: "Logs a message to the console, waits 5 minutes, and then logs another message.",
codeLink: "https://github.com/triggerdotdev/examples/blob/main/delays/src/jobs/delayJob.ts",
},
{
icon: <NamedIcon name="calendar" className={iconStyles} />,
title: "Basic interval",
description: "This Job runs every 60 seconds, starting 60 seconds after it is first indexed.",
codeLink: "https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/interval.ts",
},
{
icon: <NamedIcon name="calendar" className={iconStyles} />,
title: "Cron scheduled interval",
description: "A scheduled Job which runs at 2:30pm every Monday.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/cronScheduled.ts",
},
{
icon: <NamedIcon name="openai" className={iconStyles} />,
title: "OpenAI text summarizer",
description:
"Summarizes a block of text, pulling out the most unique / helpful points using OpenAI.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/openai-text-summarizer/src/jobs/textSummarizer.ts",
},
{
icon: <NamedIcon name="openai" className={iconStyles} />,
title: "Tell me a joke using OpenAI",
description: "Generates a random joke using OpenAI GPT 3.5.",
codeLink: "https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/tellMeAJoke.ts",
},
{
icon: <NamedIcon name="openai" className={iconStyles} />,
title: "Generate a random image using OpenAI",
description: "Generates a random image of a hedgehog using OpenAI DALL-E.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/generateHedgehogImages.ts",
},
{
icon: <NamedIcon name="resend" className={iconStyles} />,
title: "Send an email using Resend",
description: "Send a basic email using Resend.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/resend/src/jobs/resendBasicEmail.ts",
},
{
icon: <NamedIcon name="github" className={iconStyles} />,
title: "GitHub issue reminder",
description: "Sends a Slack message if a GitHub issue is left for 24h.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/github-issue-reminder/jobs/githubIssue.ts",
},
{
icon: <NamedIcon name="github" className={iconStyles} />,
title: "Github new star alert in Slack",
description: "When a repo is starred, a message is sent to a Slack.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarToSlack.ts",
},
{
icon: <NamedIcon name="github" className={iconStyles} />,
title: "Add a custom label to a GitHub issue",
description: "When a new GitHub issue is opened it adds a “Bug” label to it.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/onIssueOpened.ts",
},
{
icon: <NamedIcon name="github" className={iconStyles} />,
title: "GitHub new star alert",
description: "When a repo is starred a message is logged with the new Stargazers count.",
codeLink: "https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarAlert.ts",
},
{
icon: <NamedIcon name="slack" className={iconStyles} />,
title: "Send a Slack message",
description: "Sends a Slack message to a specific channel when an event is received.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/slack/src/jobs/sendSlackMessage.ts",
},
];
@@ -0,0 +1,121 @@
import { conform, useForm, useInputEvent } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher } from "@remix-run/react";
import { InlineCode } from "~/components/code/InlineCode";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { FormError } from "~/components/primitives/FormError";
import { Header1, Header2 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Sheet,
SheetBody,
SheetContent,
SheetHeader,
SheetTrigger,
} from "~/components/primitives/Sheet";
import { TextLink } from "~/components/primitives/TextLink";
import { docsPath } from "~/utils/pathBuilder";
import { bodySchema } from "../resources.projects.$projectId.endpoint";
import { RuntimeEnvironment, RuntimeEnvironmentType } from "@trigger.dev/database";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { useRef, useState } from "react";
type FirstEndpointSheetProps = {
projectId: string;
environments: { id: string; type: RuntimeEnvironmentType }[];
};
export function FirstEndpointSheet({ projectId, environments }: FirstEndpointSheetProps) {
const setEndpointUrlFetcher = useFetcher();
const [form, { url, environmentId }] = useForm({
id: "new-endpoint-url",
lastSubmission: setEndpointUrlFetcher.data,
onValidate({ formData }) {
return parse(formData, { schema: bodySchema });
},
});
const loadingEndpointUrl = setEndpointUrlFetcher.state !== "idle";
return (
<Sheet>
<SheetTrigger>
<ButtonContent variant={"primary/medium"}>Add your first endpoint</ButtonContent>
</SheetTrigger>
<SheetContent size="lg">
<SheetHeader>
<div>
<Header1>Add your first endpoint</Header1>
<Paragraph variant="small">
We recommend you use{" "}
<TextLink href={docsPath("documentation/guides/cli")}>the CLI</TextLink> when working
in development.
</Paragraph>
</div>
</SheetHeader>
<SheetBody>
<setEndpointUrlFetcher.Form
method="post"
action={`/resources/projects/${projectId}/endpoint`}
{...form.props}
>
<InputGroup className="mb-4 max-w-none">
<Header2>Environment type</Header2>
<SelectGroup>
<Select name={"environmentId"} defaultValue={environments[0].id}>
<SelectTrigger size="secondary/small">
<SelectValue placeholder="Select environment" className="m-0 p-0" /> Environment
</SelectTrigger>
<SelectContent>
{environments.map((environment) => (
<SelectItem key={environment.id} value={environment.id}>
<EnvironmentLabel environment={environment} />
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<FormError id={environmentId.errorId}>{environmentId.error}</FormError>
</InputGroup>
<InputGroup className="max-w-none">
<Header2>Endpoint URL</Header2>
<div className="flex items-center">
<Input
className="rounded-r-none"
{...conform.input(url, { type: "url" })}
placeholder="URL for your Trigger API route"
/>
<Button
type="submit"
variant="primary/medium"
className="rounded-l-none"
disabled={loadingEndpointUrl}
LeadingIcon={loadingEndpointUrl ? "spinner-white" : undefined}
>
{loadingEndpointUrl ? "Saving" : "Save"}
</Button>
</div>
<FormError id={url.errorId}>{url.error}</FormError>
<FormError id={form.errorId}>{form.error}</FormError>
<Hint>
This is the URL of your Trigger API route, Typically this would be:{" "}
<InlineCode variant="extra-small">https://yourdomain.com/api/trigger</InlineCode>.
</Hint>
</InputGroup>
</setEndpointUrlFetcher.Form>
</SheetBody>
</SheetContent>
</Sheet>
);
}
@@ -7,7 +7,7 @@ import { EnvironmentLabel, environmentTitle } from "~/components/environments/En
import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { ButtonContent } from "~/components/primitives/Buttons";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header2, Header3 } from "~/components/primitives/Headers";
@@ -39,6 +39,7 @@ import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
import { Badge } from "~/components/primitives/Badge";
import { FirstEndpointSheet } from "./FirstEndpointSheet";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -95,6 +96,13 @@ export default function Page() {
};
}, [selected, clients]);
const isAnyClientFullyConfigured = useMemo(() => {
return clients.some((client) => {
const { DEVELOPMENT, PRODUCTION } = client.endpoints;
return PRODUCTION.state === "configured" && DEVELOPMENT.state === PRODUCTION.state;
});
}, [clients]);
const organization = useOrganization();
const project = useProject();
@@ -119,7 +127,7 @@ export default function Page() {
<PageDescription>API Keys and endpoints for your environments.</PageDescription>
</PageHeader>
<PageBody>
<Help defaultOpen>
<Help defaultOpen={!isAnyClientFullyConfigured}>
{(open) => (
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div>
@@ -202,7 +210,12 @@ export default function Page() {
</div>
))
) : (
<Paragraph>You have no clients yet</Paragraph>
<>
<Paragraph>Add your first endpoint</Paragraph>
<Paragraph>
<FirstEndpointSheet projectId={project.id} environments={environments} />
</Paragraph>
</>
)}
</div>
{selectedEndpoint && (
@@ -355,7 +355,7 @@ function ConnectedIntegrationsList({
<TableCell to={path}>
<DateTime date={client.createdAt} includeSeconds={false} />
</TableCell>
<TableCellChevron to={path} />
<TableCellChevron to={path} isSticky />
</TableRow>
);
})}
@@ -454,7 +454,7 @@ function IntegrationsWithMissingFields({
integration={integration}
organizationId={organizationId}
button={
<ChevronRightIcon className="h-4 w-4 text-slate-700 transition group-hover:text-bright" />
<ChevronRightIcon className="h-4 w-4 text-dimmed transition group-hover:text-bright" />
}
callbackUrl={callbackUrl}
existingIntegration={client}
@@ -482,7 +482,7 @@ function AddIntegrationConnection({
icon?: string;
}) {
return (
<div className="group flex h-11 w-full items-center gap-2 rounded-md p-1 pr-3 transition hover:bg-slate-850">
<div className="group flex h-11 w-full items-center gap-2 rounded-md p-1 pr-3 transition hover:bg-slate-900">
<NamedIconInBox
name={icon ?? identifier}
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
@@ -1,11 +1,68 @@
import { ComingSoon } from "~/components/ComingSoon";
import { JobEnvironment, JobStatusTable } from "~/components/JobsStatusTable";
import { HowToDisableAJob } from "~/components/helpContent/HelpContentText";
import { DeleteJobDialogContent } from "~/components/jobs/DeleteJobModalContent";
import { Button } from "~/components/primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
import { Header2 } from "~/components/primitives/Headers";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Paragraph } from "~/components/primitives/Paragraph";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { projectJobsPath, projectPath } from "~/utils/pathBuilder";
export default function Page() {
const job = useJob();
const organization = useOrganization();
const project = useProject();
return (
<ComingSoon
title="Job settings"
description="Disable a Job, archive it and more."
icon="settings"
/>
<Help defaultOpen>
{(open) => (
<div className={cn("grid h-fit gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div className="w-full">
<div className="flex items-center justify-between">
<Header2 className="mb-2 flex items-center gap-1">Environments</Header2>
<HelpTrigger title="How do disable a Job?" />
</div>
<JobStatusTable environments={job.environments} />
<div className="mt-4 flex w-full items-center justify-end gap-x-3">
{job.status === "ACTIVE" && (
<Paragraph variant="small">
Disable this Job in all environments before deleting
</Paragraph>
)}
<Dialog>
<DialogTrigger asChild>
<Button
variant="danger/small"
leadingIconClassName="text-bright"
LeadingIcon="trash-can"
>
I want to delete this Job
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DeleteJobDialogContent
title={job.title}
slug={job.slug}
environments={job.environments}
id={job.id}
redirectTo={projectJobsPath(organization, project)}
/>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
</div>
<HelpContent title="How to disable a Job">
<HowToDisableAJob id={job.slug} version={job.version} name={job.title} />
</HelpContent>
</div>
)}
</Help>
);
}
@@ -2,6 +2,7 @@ import { Outlet, useLocation } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { Fragment } from "react";
import { typedjson } from "remix-typedjson";
import { JobStatusBadge } from "~/components/jobs/JobStatusBadge";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { JobsMenu } from "~/components/navigation/JobsMenu";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
@@ -134,6 +135,18 @@ export default function Job() {
}
/>
)}
<PageInfoProperty
icon="pulse"
label={"STATUS"}
value={
<JobStatusBadge
enabled={job.status === "ACTIVE"}
hasIntegrationsRequiringAction={job.hasIntegrationsRequiringAction}
hasRuns={job.lastRun !== undefined}
badgeSize="small"
/>
}
/>
</PageInfoGroup>
<PageInfoGroup alignment="right">
<Paragraph variant="extra-small" className="text-slate-600">
@@ -8,7 +8,7 @@ import { ProjectsMenu } from "~/components/navigation/ProjectsMenu";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { ProjectPresenter } from "~/presenters/ProjectPresenter.server";
import { analytics } from "~/services/analytics.server";
import { telemetry } from "~/services/telemetry.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { projectPath } from "~/utils/pathBuilder";
@@ -33,7 +33,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
});
}
analytics.project.identify({ project });
telemetry.project.identify({ project });
return typedjson({
project,
@@ -103,7 +103,7 @@ export default function Page() {
<OrgAdminHeader />
<PageBody>
<Header2>Members</Header2>
<ul className="flex w-full max-w-md flex-col divide-y divide-slate-800 border-b border-slate-800">
<ul className="flex w-full max-w-md flex-col divide-y divide-uiBorder border-b border-uiBorder">
{members.map((member) => (
<li key={member.user.id} className="flex items-center gap-x-4 py-4">
<UserAvatar
@@ -6,7 +6,7 @@ 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 { telemetry } from "~/services/telemetry.server";
import { commitCurrentOrgSession, setCurrentOrg } from "~/services/currentOrganization.server";
import { requireUserId } from "~/services/session.server";
import { organizationPath } from "~/utils/pathBuilder";
@@ -25,7 +25,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
throw new Response("Not Found", { status: 404 });
}
analytics.organization.identify({ organization });
telemetry.organization.identify({ organization });
const session = await setCurrentOrg(organization.slug, request);
+1 -10
View File
@@ -6,26 +6,17 @@ import { ImpersonationBanner } from "~/components/ImpersonationBanner";
import { NoMobileOverlay } from "~/components/NoMobileOverlay";
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, invitationCodePath } from "~/utils/pathBuilder";
import { confirmBasicDetailsPath } 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) {
@@ -19,7 +19,10 @@ export async function action({ request }: ActionArgs) {
},
body: JSON.stringify({
title: body.type,
content: body,
content: {
...body,
example: { id: body.type, name: body.type, icon: "stripe", payload: body.data.object },
},
readOnly: true,
}),
});
@@ -1,4 +1,5 @@
import { ActionArgs, LoaderArgs, json } from "@remix-run/server-runtime";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { z } from "zod";
import { PrismaClient, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
@@ -99,6 +100,9 @@ export class TriggerEndpointIndexHookService {
slug: endpointSlug,
},
},
include: {
environment: true,
},
});
if (!endpoint) {
@@ -122,6 +126,8 @@ export class TriggerEndpointIndexHookService {
},
{
runAt: new Date(Date.now() + 5000),
maxAttempts:
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
}
);
}
@@ -0,0 +1,52 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { eventRecordToApiJson } from "~/api.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CancelEventService } from "~/services/events/cancelEvent.server";
import { logger } from "~/services/logger.server";
const ParamsSchema = z.object({
eventId: z.string(),
});
export async function action({ request, params }: ActionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const authenticatedEnv = authenticationResult.environment;
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return json({ error: "Invalid or Missing eventId" }, { status: 400 });
}
const { eventId } = parsed.data;
const service = new CancelEventService();
try {
const updatedEvent = await service.call(authenticatedEnv, eventId);
if (!updatedEvent) {
return json({ error: "Event not found" }, { status: 404 });
}
return json(eventRecordToApiJson(updatedEvent));
} catch (err) {
logger.error("CancelEventService.call() error", {
error: err,
});
return json({ error: "Internal Server Error" }, { status: 500 });
}
}
@@ -1,6 +1,6 @@
import type { ActionArgs, LoaderArgs } from "@remix-run/server-runtime";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { cors } from "remix-utils";
import { GetEvent } from "@trigger.dev/core";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -32,9 +32,36 @@ export async function loader({ request, params }: LoaderArgs) {
const { eventId } = parsed.data;
const event = await prisma.eventRecord.findFirst({
const event = await findEventRecord(eventId, authenticatedEnv.id);
if (!event) {
return apiCors(request, json({ error: "Event not found" }, { status: 404 }));
}
return apiCors(request, json(toJSON(event)));
}
function toJSON(eventRecord: FoundEventRecord): GetEvent {
return {
id: eventRecord.eventId,
name: eventRecord.name,
createdAt: eventRecord.createdAt,
updatedAt: eventRecord.updatedAt,
runs: eventRecord.runs.map((run) => ({
id: run.id,
status: run.status,
startedAt: run.startedAt,
completedAt: run.completedAt,
})),
};
}
type FoundEventRecord = NonNullable<Awaited<ReturnType<typeof findEventRecord>>>;
async function findEventRecord(eventId: string, environmentId: string) {
return await prisma.eventRecord.findUnique({
select: {
id: true,
eventId: true,
name: true,
createdAt: true,
updatedAt: true,
@@ -48,14 +75,10 @@ export async function loader({ request, params }: LoaderArgs) {
},
},
where: {
id: eventId,
environmentId: authenticatedEnv.id,
eventId_environmentId: {
eventId,
environmentId,
},
},
});
if (!event) {
return apiCors(request, json({ error: "Event not found" }, { status: 404 }));
}
return apiCors(request, json(event));
}
+6 -1
View File
@@ -4,6 +4,7 @@ import { SendEventBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { IngestSendEvent } from "~/services/events/ingestSendEvent.server";
import { eventRecordToApiJson } from "~/api.server";
export async function action({ request }: ActionArgs) {
// Ensure this is a POST request
@@ -33,5 +34,9 @@ export async function action({ request }: ActionArgs) {
const event = await service.call(authenticatedEnv, body.data.event, body.data.options);
return json(event);
if (!event) {
return json({ error: "Failed to create event" }, { status: 500 });
}
return json(eventRecordToApiJson(event));
}
@@ -107,6 +107,32 @@ export class RunTaskService {
});
if (existingTask) {
if (existingTask.status === "CANCELED") {
const existingTaskStatus =
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
const resumedExistingTask = await tx.task.update({
where: {
id: existingTask.id,
},
data: {
status: existingTaskStatus,
startedAt: new Date(),
completedAt: existingTaskStatus === "COMPLETED" ? new Date() : undefined,
},
include: {
run: true,
attempts: true,
},
});
return resumedExistingTask;
}
return existingTask;
}
-160
View File
@@ -1,160 +0,0 @@
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/TextLink";
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 login and access now.
<br /> No code yet? You can{" "}
<TextLink
target="_blank"
href="https://bcymafitv0e.typeform.com/to/QQnotGJM#source=cloud-beta"
>
request a code
</TextLink>{" "}
or get started now by{" "}
<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-hosting
</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>
);
}
@@ -57,6 +57,12 @@ export async function action({ request, params }: ActionArgs) {
return json(submission);
}
return json(e, { status: 400 });
if (e instanceof Error) {
submission.error.url = `${e.name}: ${e.message}`;
} else {
submission.error.url = "Unknown error";
}
return json(submission, { status: 400 });
}
}
@@ -0,0 +1,74 @@
import { ActionFunction } from "@remix-run/node";
import { z } from "zod";
import { prisma } from "~/db.server";
import {
jsonWithErrorMessage,
jsonWithSuccessMessage,
redirectWithSuccessMessage,
} from "~/models/message.server";
import { DeleteJobService } from "~/services/jobs/deleteJob.server";
import { logger } from "~/services/logger.server";
import { requireUserId } from "~/services/session.server";
const ParamSchema = z.object({
jobId: z.string(),
});
export const action: ActionFunction = async ({ request, params }) => {
const { jobId } = ParamSchema.parse(params);
const userId = await requireUserId(request);
// Find the job
const job = await prisma.job.findFirst({
where: {
id: jobId,
organization: {
members: {
some: {
userId,
},
},
},
},
});
if (!job) {
return jsonWithErrorMessage({ ok: false }, request, `Job could not be scheduled for deletion.`);
}
try {
const deleteJobService = new DeleteJobService();
await deleteJobService.call(job);
const url = new URL(request.url);
const redirectTo = url.searchParams.get("redirectTo");
logger.debug("Job scheduled for deletion", {
url,
redirectTo,
job,
});
if (typeof redirectTo === "string" && redirectTo.length > 0) {
return redirectWithSuccessMessage(
redirectTo,
request,
`Job ${job.slug} has been scheduled for deletion.`
);
}
return jsonWithSuccessMessage(
{ ok: true },
request,
`Job ${job.slug} has been scheduled for deletion.`
);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return jsonWithErrorMessage(
{ ok: false },
request,
`Job could not be scheduled for deletion: ${message}`
);
}
};
@@ -0,0 +1,71 @@
import { parse } from "@conform-to/zod";
import { ActionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import {
CreateEndpointError,
CreateEndpointService,
} from "~/services/endpoints/createEndpoint.server";
import { requireUserId } from "~/services/session.server";
import { RuntimeEnvironmentTypeSchema } from "@trigger.dev/core";
import { env } from "process";
import { ValidateCreateEndpointService } from "~/services/endpoints/validateCreateEndpoint.server";
const ParamsSchema = z.object({
projectId: z.string(),
});
export const bodySchema = z.object({
environmentId: z.string(),
url: z.string().url("Must be a valid URL"),
});
export async function action({ request, params }: ActionArgs) {
const userId = await requireUserId(request);
const { projectId } = ParamsSchema.parse(params);
const formData = await request.formData();
const submission = parse(formData, { schema: bodySchema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
try {
const environment = await prisma.runtimeEnvironment.findUnique({
include: {
organization: true,
project: true,
},
where: {
id: submission.value.environmentId,
},
});
if (!environment) {
submission.error.environmentId = "Environment not found";
return json(submission);
}
const service = new ValidateCreateEndpointService();
const result = await service.call({
url: submission.value.url,
environment,
});
return json(submission);
} catch (e) {
if (e instanceof CreateEndpointError) {
submission.error.url = e.message;
return json(submission);
}
if (e instanceof Error) {
submission.error.url = `${e.name}: ${e.message}`;
} else {
submission.error.url = "Unknown error";
}
return json(submission, { status: 400 });
}
}
@@ -0,0 +1,60 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
import { sse } from "~/utils/sse";
export async function loader({ request, params }: LoaderArgs) {
await requireUserId(request);
const { projectId } = z.object({ projectId: z.string() }).parse(params);
const project = await projectForUpdates(projectId);
if (!project) {
return new Response("Not found", { status: 404 });
}
let lastSignals = calculateChangeSignals(project);
return sse({
request,
run: async (send, stop) => {
const result = await projectForUpdates(projectId);
if (!result) {
return stop();
}
const newSignals = calculateChangeSignals(result);
if (lastSignals.jobCount !== newSignals.jobCount) {
send({ data: JSON.stringify(newSignals) });
}
lastSignals = newSignals;
},
});
}
function projectForUpdates(id: string) {
return prisma.project.findUnique({
where: {
id,
},
include: {
_count: {
select: { jobs: true },
},
},
});
}
function calculateChangeSignals(
project: NonNullable<Awaited<ReturnType<typeof projectForUpdates>>>
) {
const jobCount = project._count?.jobs ?? 0;
return {
jobCount,
};
}
@@ -2,6 +2,7 @@ import { parse } from "@conform-to/zod";
import { ActionFunction, json } from "@remix-run/node";
import { z } from "zod";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { logger } from "~/services/logger.server";
import { CancelRunService } from "~/services/runs/cancelRun.server";
export const cancelSchema = z.object({
@@ -31,7 +32,19 @@ export const action: ActionFunction = async ({ request, params }) => {
request,
`Canceled run. Any pending tasks will be canceled.`
);
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
} catch (error) {
if (error instanceof Error) {
logger.error("Failed to cancel run", {
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
});
return json({ errors: { body: error.message } }, { status: 400 });
} else {
logger.error("Failed to cancel run", { error });
return json({ errors: { body: "Unknown error" } }, { status: 400 });
}
}
};
@@ -0,0 +1,67 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { logger } from "~/services/logger.server";
import { sse } from "~/utils/sse";
export async function loader({ request }: LoaderArgs) {
const url = new URL(request.url);
const searchParams = Object.fromEntries(url.searchParams.entries());
const options = z
.object({
minDelay: z.coerce.number().int(),
maxDelay: z.coerce.number().int(),
undefinedProbability: z.coerce.number().min(0).max(1).default(0.1),
})
.parse(searchParams);
logger.debug("Test SSE stream", { options });
let lastSignals = calculateChangeSignals(Date.now());
return sse({
request,
run: async (send, stop) => {
const result = await dateForUpdates(options);
if (!result) {
return stop();
}
const newSignals = calculateChangeSignals(result);
if (lastSignals.ts !== newSignals.ts) {
send({ data: JSON.stringify(newSignals) });
}
lastSignals = newSignals;
},
});
}
async function dateForUpdates(opts: {
minDelay: number;
maxDelay: number;
undefinedProbability: number;
}): Promise<number | undefined> {
// Randomly await between minDelay and maxDelay
await new Promise((resolve) => {
setTimeout(resolve, Math.random() * (opts.maxDelay - opts.minDelay) + opts.minDelay);
});
// There should be about a x% chance that this returns undefined
if (Math.random() < opts.undefinedProbability) {
logger.debug("Test SSE dataForUpdates returning undefined");
return undefined;
}
// Randomly return true or false
return Date.now();
}
function calculateChangeSignals(ts: number) {
return {
ts,
};
}
+37
View File
@@ -0,0 +1,37 @@
import { useLoaderData } from "@remix-run/react";
import { LoaderArgs } from "@remix-run/server-runtime";
import { useEventSource } from "remix-utils";
import { z } from "zod";
export async function loader({ request }: LoaderArgs) {
const url = new URL(request.url);
const params = Object.fromEntries(url.searchParams.entries());
const config = z
.object({
minDelay: z.coerce.number().int().min(0).max(10000).default(1000),
maxDelay: z.coerce.number().int().min(0).max(10000).default(2000),
undefinedProbability: z.coerce.number().min(0).max(1).default(0.1),
})
.parse(params);
return config;
}
export default function SSETest() {
const { minDelay, maxDelay, undefinedProbability } = useLoaderData<typeof loader>();
const events = useEventSource(
`/tests/sse/stream?minDelay=${minDelay}&maxDelay=${maxDelay}&undefinedProbability=${undefinedProbability}`,
{
event: "message",
}
);
return (
<div>
<h2>SSE Test</h2>
<p>{events ?? "No events"}</p>
</div>
);
}
@@ -1,364 +0,0 @@
import { PostHog } from "posthog-node";
import { env } from "~/env.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import type { User } from "~/models/user.server";
class BehaviouralAnalytics {
client: PostHog | undefined = undefined;
constructor(apiKey?: string) {
if (!apiKey) {
console.log("No PostHog API key, so analytics won't track");
return;
}
this.client = new PostHog(apiKey, { host: "https://app.posthog.com" });
}
user = {
identify: ({ user, isNewUser }: { user: User; isNewUser: boolean }) => {
if (this.client === undefined) return;
this.client.identify({
distinctId: user.id,
properties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
isNewUser,
},
});
if (isNewUser) {
this.#capture({
userId: user.id,
event: "user created",
eventProperties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
},
});
}
},
};
organization = {
identify: ({ organization }: { organization: Organization }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "organization",
groupKey: organization.id,
properties: {
name: organization.title,
slug: organization.slug,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
});
},
new: ({
userId,
organization,
organizationCount,
}: {
userId: string;
organization: Organization;
organizationCount: number;
}) => {
if (this.client === undefined) return;
this.#capture({
userId,
event: "organization created",
organizationId: organization.id,
eventProperties: {
id: organization.id,
slug: organization.slug,
title: organization.title,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
userProperties: {
organizationCount: organizationCount,
},
});
},
};
project = {
identify: ({ project }: { project: Project }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "project",
groupKey: project.id,
properties: {
name: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
new: ({
userId,
organizationId,
project,
}: {
userId: string;
organizationId: string;
project: Project;
}) => {
if (this.client === undefined) return;
this.#capture({
userId,
event: "project created",
organizationId,
eventProperties: {
id: project.id,
title: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
};
//todo Job
// workflow = {
// identify: ({ workflow }: { workflow: Workflow }) => {
// if (this.client === undefined) return;
// this.client.groupIdentify({
// groupType: "workflow",
// groupKey: workflow.id,
// properties: {
// name: workflow.title,
// slug: workflow.slug,
// packageJson: workflow.packageJson,
// jsonSchema: workflow.jsonSchema,
// createdAt: workflow.createdAt,
// updatedAt: workflow.updatedAt,
// organizationId: workflow.organizationId,
// type: workflow.type,
// status: workflow.status,
// externalSourceId: workflow.externalSourceId,
// service: workflow.service,
// eventNames: workflow.eventNames,
// disabledAt: workflow.disabledAt,
// archivedAt: workflow.archivedAt,
// isArchived: workflow.isArchived,
// triggerTtlInSeconds: workflow.triggerTtlInSeconds,
// },
// });
// },
// new: ({
// userId,
// organizationId,
// workflow,
// workflowCount,
// }: {
// userId: string;
// organizationId: string;
// workflow: Workflow;
// workflowCount: number;
// }) => {
// if (this.client === undefined) return;
// this.#capture({
// userId,
// event: "workflow created",
// organizationId: organizationId,
// jobId: workflow.id,
// eventProperties: {
// id: workflow.id,
// slug: workflow.slug,
// title: workflow.title,
// packageJson: workflow.packageJson,
// jsonSchema: workflow.jsonSchema,
// createdAt: workflow.createdAt,
// updatedAt: workflow.updatedAt,
// organizationId: workflow.organizationId,
// type: workflow.type,
// status: workflow.status,
// externalSourceId: workflow.externalSourceId,
// service: workflow.service,
// eventNames: workflow.eventNames,
// disabledAt: workflow.disabledAt,
// archivedAt: workflow.archivedAt,
// isArchived: workflow.isArchived,
// triggerTtlInSeconds: workflow.triggerTtlInSeconds,
// },
// userProperties: {
// workflowCount: workflowCount,
// },
// });
// },
// };
// workflowRun = {
// new: ({
// userId,
// organizationId,
// workflowId,
// workflowRun,
// environmentType,
// runCount,
// }: {
// userId: string;
// organizationId: string;
// workflowId: string;
// workflowRun: WorkflowRun;
// environmentType: string;
// runCount: number;
// }) => {
// if (this.client === undefined) return;
// this.#capture({
// userId,
// event: "workflow run created",
// eventProperties: {
// id: workflowRun.id,
// workflowId: workflowRun.workflowId,
// environmentId: workflowRun.environmentId,
// environmentType,
// eventRuleId: workflowRun.eventRuleId,
// eventId: workflowRun.eventId,
// error: workflowRun.error,
// status: workflowRun.status,
// attemptCount: workflowRun.attemptCount,
// createdAt: workflowRun.createdAt,
// updatedAt: workflowRun.updatedAt,
// startedAt: workflowRun.startedAt,
// finishedAt: workflowRun.finishedAt,
// timedOutAt: workflowRun.timedOutAt,
// timedOutReason: workflowRun.timedOutReason,
// isTest: workflowRun.isTest,
// },
// userProperties: {
// runCount: runCount,
// },
// organizationId: organizationId,
// jobId: workflowId,
// environmentId: workflowRun.environmentId,
// });
// },
// };
environment = {
identify: ({ environment }: { environment: RuntimeEnvironment }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "environment",
groupKey: environment.id,
properties: {
name: environment.slug,
slug: environment.slug,
organizationId: environment.organizationId,
createdAt: environment.createdAt,
updatedAt: environment.updatedAt,
},
});
},
};
telemetry = {
capture: ({
userId,
event,
properties,
organizationId,
environmentId,
}: {
userId: string;
event: string;
properties: Record<string | number, any>;
organizationId?: string;
environmentId?: string;
}) => {
this.#capture({
userId,
event,
eventProperties: properties,
organizationId,
environmentId,
});
},
};
#capture(event: CaptureEvent) {
if (this.client === undefined) return;
let groups: Record<string, string> = {};
if (event.organizationId) {
groups = {
...groups,
organization: event.organizationId,
};
}
if (event.projectId) {
groups = {
...groups,
project: event.projectId,
};
}
if (event.jobId) {
groups = {
...groups,
workflow: event.jobId,
};
}
if (event.environmentId) {
groups = {
...groups,
environment: event.environmentId,
};
}
let properties: Record<string, any> = {};
if (event.eventProperties) {
properties = {
...properties,
...event.eventProperties,
};
}
if (event.userProperties) {
properties = {
...properties,
$set: event.userProperties,
};
}
if (event.userOnceProperties) {
properties = {
...properties,
$set_once: event.userOnceProperties,
};
}
const eventData = {
distinctId: event.userId,
event: event.event,
properties,
groups,
};
this.client.capture(eventData);
}
}
type CaptureEvent = {
userId: string;
event: string;
organizationId?: string;
projectId?: string;
jobId?: string;
environmentId?: string;
eventProperties?: Record<string, any>;
userProperties?: Record<string, any>;
userOnceProperties?: Record<string, any>;
};
export const analytics = new BehaviouralAnalytics(env.POSTHOG_PROJECT_KEY);
+6
View File
@@ -1,6 +1,7 @@
import type { DeliverEmail } from "emails";
import { EmailClient } from "emails";
import type { SendEmailOptions } from "remix-auth-email-link";
import { redirect } from "remix-typedjson";
import { env } from "~/env.server";
import type { User } from "~/models/user.server";
import type { AuthUser } from "./authUser";
@@ -14,6 +15,11 @@ const client = new EmailClient({
});
export async function sendMagicLinkEmail(options: SendEmailOptions<AuthUser>): Promise<void> {
// Auto redirect when in development mode
if (env.NODE_ENV === "development") {
throw redirect(options.magicLink);
}
return client.send({
email: "magic_link",
to: options.emailAddress,
+83 -11
View File
@@ -13,8 +13,10 @@ import {
RegisterTriggerBodySchema,
RunJobBody,
RunJobResponseSchema,
ValidateResponse,
ValidateResponseSchema,
} from "@trigger.dev/core";
import { safeBodyFromResponse } from "~/utils/json";
import { safeBodyFromResponse, safeParseBodyFromResponse } from "~/utils/json";
import { logger } from "./logger.server";
export class EndpointApiError extends Error {
@@ -25,20 +27,18 @@ export class EndpointApiError extends Error {
}
}
// TODO: this should work with tunnelling
export class EndpointApi {
constructor(
private apiKey: string,
private url: string,
private id: string
private url: string
) {}
async ping(): Promise<PongResponse> {
async ping(endpointId: string): Promise<PongResponse> {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"x-trigger-api-key": this.apiKey,
"x-trigger-endpoint-id": this.id,
"x-trigger-endpoint-id": endpointId,
"x-trigger-action": "PING",
},
});
@@ -73,13 +73,23 @@ export class EndpointApi {
};
}
const anyBody = await response.json();
const pongResponse = await safeParseBodyFromResponse(response, PongResponseSchema);
logger.debug("ping() response from endpoint", {
body: anyBody,
});
if (!pongResponse) {
return {
ok: false,
error: `Could not parse response from endpoint. Make sure it points to the correct URL (you might be missing /api/trigger)`,
};
}
return PongResponseSchema.parse(anyBody);
if (!pongResponse.success) {
return {
ok: false,
error: `Endpoint ${this.url} responded with error: ${pongResponse.error.message}`,
};
}
return pongResponse.data;
}
async indexEndpoint() {
@@ -155,6 +165,10 @@ export class EndpointApi {
}
async executeJobRequest(options: RunJobBody) {
logger.debug("executeJobRequest()", {
options,
});
const response = await safeFetch(this.url, {
method: "POST",
headers: {
@@ -265,6 +279,64 @@ export class EndpointApi {
return HttpSourceResponseSchema.parse(anyBody);
}
async validate(): Promise<ValidateResponse> {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"x-trigger-api-key": this.apiKey,
"x-trigger-action": "VALIDATE",
},
});
if (!response) {
return {
ok: false,
error: `Could not connect to endpoint ${this.url}`,
};
}
if (response.status === 401) {
const body = await safeBodyFromResponse(response, ErrorWithStackSchema);
if (body) {
return {
ok: false,
error: body.message,
} as const;
}
return {
ok: false,
error: `Trigger API key is invalid`,
} as const;
}
if (!response.ok) {
return {
ok: false,
error: `Could not connect to endpoint ${this.url}. Status code: ${response.status}`,
};
}
const validateResponse = await safeParseBodyFromResponse(response, ValidateResponseSchema);
if (!validateResponse) {
return {
ok: false,
error: `Could not parse response from endpoint. Make sure it points to the correct URL (you might be missing /api/trigger)`,
};
}
if (!validateResponse.success) {
return {
ok: false,
error: `Endpoint ${this.url} responded with error: ${validateResponse.error.message}`,
};
}
return validateResponse.data;
}
}
async function safeFetch(url: string, options: RequestInit) {
@@ -4,6 +4,7 @@ import { AuthenticatedEnvironment } from "../apiAuth.server";
import { EndpointApi } from "../endpointApi.server";
import { workerQueue } from "../worker.server";
import { env } from "~/env.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
@@ -34,9 +35,9 @@ export class CreateEndpointService {
}) {
const endpointUrl = this.#normalizeEndpointUrl(url);
const client = new EndpointApi(environment.apiKey, endpointUrl, id);
const client = new EndpointApi(environment.apiKey, endpointUrl);
const pong = await client.ping();
const pong = await client.ping(id);
if (!pong.ok) {
throw new CreateEndpointError("FAILED_PING", pong.error);
@@ -51,6 +52,9 @@ export class CreateEndpointService {
slug: id,
},
},
include: {
environment: true,
},
create: {
environment: {
connect: {
@@ -83,7 +87,11 @@ export class CreateEndpointService {
id: endpoint.id,
source: "INTERNAL",
},
{ tx }
{
tx,
maxAttempts:
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
}
);
return endpoint;
@@ -7,10 +7,12 @@ import { logger } from "../logger.server";
import { RegisterSourceService } from "../sources/registerSource.server";
import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server";
import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
import { DisableJobService } from "../jobs/disableJob.server";
export class IndexEndpointService {
#prismaClient: PrismaClient;
#registerJobService = new RegisterJobService();
#disableJobService = new DisableJobService();
#registerSourceService = new RegisterSourceService();
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
@@ -28,7 +30,7 @@ export class IndexEndpointService {
const endpoint = await findEndpoint(id);
// Make a request to the endpoint to fetch a list of jobs
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url, endpoint.slug);
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url);
const indexResponse = await client.indexEndpoint();
@@ -57,23 +59,98 @@ export class IndexEndpointService {
sources: 0,
dynamicTriggers: 0,
dynamicSchedules: 0,
disabledJobs: 0,
};
const existingJobs = await this.#prismaClient.job.findMany({
where: {
projectId: endpoint.projectId,
deletedAt: null,
},
include: {
aliases: {
where: {
name: "latest",
environmentId: endpoint.environmentId,
},
include: {
version: true,
},
take: 1,
},
},
});
for (const job of jobs) {
if (!job.enabled) {
continue;
const disabledJob = await this.#disableJobService
.call(endpoint, { slug: job.id, version: job.version })
.catch((error) => {
logger.error("Failed to disable job", {
endpointId: endpoint.id,
job,
error,
});
return;
});
if (disabledJob) {
indexStats.disabledJobs++;
}
} else {
try {
const registeredVersion = await this.#registerJobService.call(endpoint, job);
if (registeredVersion) {
indexStats.jobs++;
}
} catch (error) {
logger.error("Failed to register job", {
endpointId: endpoint.id,
job,
error,
});
}
}
}
try {
await this.#registerJobService.call(endpoint, job);
// TODO: we need to do this for sources, dynamic triggers, and dynamic schedules
const missingJobs = existingJobs.filter((job) => {
return !jobs.find((j) => j.id === job.slug);
});
indexStats.jobs++;
} catch (error) {
logger.error("Failed to register job", {
endpointId: endpoint.id,
job,
error,
});
if (missingJobs.length > 0) {
logger.debug("Disabling missing jobs", {
endpointId: endpoint.id,
missingJobIds: missingJobs.map((job) => job.slug),
});
for (const job of missingJobs) {
const latestVersion = job.aliases[0]?.version;
if (!latestVersion) {
continue;
}
const disabledJob = await this.#disableJobService
.call(endpoint, {
slug: job.slug,
version: latestVersion.version,
})
.catch((error) => {
logger.error("Failed to disable job", {
endpointId: endpoint.id,
job,
error,
});
return;
});
if (disabledJob) {
indexStats.disabledJobs++;
}
}
}
@@ -0,0 +1,44 @@
import { PrismaClient, prisma } from "~/db.server";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
export class RecurringEndpointIndexService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(ts: Date) {
// Find all production endpoints that haven't been indexed in the last 10 minutes
const currentTimestamp = ts.getTime();
const endpoints = await this.#prismaClient.endpoint.findMany({
where: {
environment: {
type: RuntimeEnvironmentType.PRODUCTION,
},
indexings: {
none: {
createdAt: {
gt: new Date(currentTimestamp - 10 * 60 * 1000),
},
},
},
},
});
logger.debug("Found endpoints that haven't been indexed in the last 10 minutes", {
count: endpoints.length,
});
// Enqueue each endpoint for indexing
for (const endpoint of endpoints) {
await workerQueue.enqueue("indexEndpoint", {
id: endpoint.id,
source: "INTERNAL",
});
}
}
}
@@ -0,0 +1,108 @@
import { customAlphabet } from "nanoid";
import { $transaction, prisma, PrismaClient } from "~/db.server";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { workerQueue } from "../worker.server";
import { CreateEndpointError } from "./createEndpoint.server";
import { EndpointApi } from "../endpointApi.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
export class ValidateCreateEndpointService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ environment, url }: { environment: AuthenticatedEnvironment; url: string }) {
const endpointUrl = this.#normalizeEndpointUrl(url);
const client = new EndpointApi(environment.apiKey, endpointUrl);
const validationResult = await client.validate();
if (!validationResult.ok) {
throw new Error(validationResult.error);
}
try {
const result = await $transaction(this.#prismaClient, async (tx) => {
const endpoint = await tx.endpoint.upsert({
where: {
environmentId_slug: {
environmentId: environment.id,
slug: validationResult.endpointId,
},
},
include: {
environment: true,
},
create: {
environment: {
connect: {
id: environment.id,
},
},
organization: {
connect: {
id: environment.organizationId,
},
},
project: {
connect: {
id: environment.projectId,
},
},
slug: validationResult.endpointId,
url: endpointUrl,
indexingHookIdentifier: indexingHookIdentifier(),
},
update: {
url: endpointUrl,
},
});
// Kick off process to fetch the jobs for this endpoint
await workerQueue.enqueue(
"indexEndpoint",
{
id: endpoint.id,
source: "INTERNAL",
},
{
tx,
maxAttempts:
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
}
);
return endpoint;
});
return result;
} catch (error) {
if (error instanceof Error) {
throw new CreateEndpointError("FAILED_UPSERT", error.message);
} else {
throw new CreateEndpointError("FAILED_UPSERT", "Something went wrong");
}
}
}
// If the endpoint URL points to localhost, and the RUNTIME_PLATFORM is docker-compose, then we need to rewrite the host to host.docker.internal
// otherwise we shouldn't change anything
#normalizeEndpointUrl(url: string) {
if (env.RUNTIME_PLATFORM === "docker-compose") {
const urlObj = new URL(url);
if (urlObj.hostname === "localhost") {
urlObj.hostname = "host.docker.internal";
return urlObj.toString();
}
}
return url;
}
}
@@ -0,0 +1,47 @@
import type { EventRecord } from "@trigger.dev/database";
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
export class CancelEventService {
#prismaClient: PrismaClientOrTransaction;
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
eventId: string
): Promise<EventRecord | undefined> {
return await $transaction(this.#prismaClient, async (tx) => {
const event = await tx.eventRecord.findUnique({
where: {
eventId_environmentId: {
eventId: eventId,
environmentId: environment.id,
},
},
});
if (!event) {
return;
}
if (event.cancelledAt) {
return event;
}
//update the cancelledAt column in the eventRecord table
const updatedEvent = await tx.eventRecord.update({
where: { id: event.id },
data: { cancelledAt: new Date() },
});
// Dequeue the event after the db has been updated
await workerQueue.dequeue(`event:${event.id}`, { tx });
return updatedEvent;
});
}
}
@@ -1,6 +1,6 @@
import type { EventDispatcher, EventRecord } from "@trigger.dev/database";
import type { EventFilter } from "@trigger.dev/core";
import { EventFilterSchema } from "@trigger.dev/core";
import { EventFilterSchema, eventFilterMatches } from "@trigger.dev/core";
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { workerQueue } from "../worker.server";
@@ -124,29 +124,6 @@ export class EventMatcher {
}
public matches(filter: EventFilter) {
return patternMatches(this.event, filter);
return eventFilterMatches(this.event, filter);
}
}
function patternMatches(payload: any, pattern: any): boolean {
for (const [patternKey, patternValue] of Object.entries(pattern)) {
const payloadValue = payload[patternKey];
if (Array.isArray(patternValue)) {
if (patternValue.length > 0 && !patternValue.includes(payloadValue)) {
return false;
}
} else if (typeof patternValue === "object") {
if (Array.isArray(payloadValue)) {
if (!payloadValue.some((item) => patternMatches(item, patternValue))) {
return false;
}
} else {
if (!patternMatches(payloadValue, patternValue)) {
return false;
}
}
}
}
return true;
}
@@ -2,6 +2,7 @@ import type { RawEvent, SendEventOptions } from "@trigger.dev/core";
import { $transaction, PrismaClientOrTransaction, PrismaErrorSchema, prisma } from "~/db.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { workerQueue } from "~/services/worker.server";
import { logger } from "../logger.server";
export class IngestSendEvent {
#prismaClient: PrismaClientOrTransaction;
@@ -36,71 +37,87 @@ export class IngestSendEvent {
try {
const deliverAt = this.#calculateDeliverAt(options);
return await $transaction(this.#prismaClient, async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.findUniqueOrThrow({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: options.accountId,
return await $transaction(
this.#prismaClient,
async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.findUniqueOrThrow({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: options.accountId,
},
},
})
: undefined;
// Create a new event in the database
const eventLog = await tx.eventRecord.create({
data: {
organization: {
connect: {
id: environment.organizationId,
},
},
})
: undefined;
// Create a new event in the database
const eventLog = await tx.eventRecord.create({
data: {
organization: {
connect: {
id: environment.organizationId,
project: {
connect: {
id: environment.projectId,
},
},
},
project: {
connect: {
id: environment.projectId,
environment: {
connect: {
id: environment.id,
},
},
eventId: event.id,
name: event.name,
timestamp: event.timestamp ?? new Date(),
payload: event.payload ?? {},
context: event.context ?? {},
source: event.source ?? "trigger.dev",
sourceContext,
deliverAt: deliverAt,
externalAccount: externalAccount
? {
connect: {
id: externalAccount.id,
},
}
: {},
},
environment: {
connect: {
id: environment.id,
});
if (this.deliverEvents) {
// Produce a message to the event bus
await workerQueue.enqueue(
"deliverEvent",
{
id: eventLog.id,
},
},
eventId: event.id,
name: event.name,
timestamp: event.timestamp ?? new Date(),
payload: event.payload ?? {},
context: event.context ?? {},
source: event.source ?? "trigger.dev",
sourceContext,
deliverAt: deliverAt,
externalAccount: externalAccount
? {
connect: {
id: externalAccount.id,
},
}
: {},
},
});
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
);
}
if (this.deliverEvents) {
// Produce a message to the event bus
await workerQueue.enqueue(
"deliverEvent",
{
id: eventLog.id,
},
{ runAt: eventLog.deliverAt, tx }
);
}
return eventLog;
});
return eventLog;
},
{ rethrowPrismaErrors: true }
);
} catch (error) {
const prismaError = PrismaErrorSchema.safeParse(error);
if (!prismaError.success) {
logger.debug("Error parsing prisma error", {
error,
parseError: prismaError.error.format(),
});
throw error;
}
// If the error is a Prisma unique constraint error, it means that the event already exists
if (prismaError.success && prismaError.data.code === "P2002") {
logger.debug("Event already exists, finding and returning", { event, environment });
return this.#prismaClient.eventRecord.findUniqueOrThrow({
where: {
eventId_environmentId: {
@@ -4,6 +4,7 @@ import { plain } from "./integrations/plain";
import { resend } from "./integrations/resend";
import { slack } from "./integrations/slack";
import { stripe } from "./integrations/stripe";
import { sendgrid } from "./integrations/sendgrid";
import { supabaseManagement, supabase } from "./integrations/supabase";
import { typeform } from "./integrations/typeform";
import type { Integration } from "./types";
@@ -34,8 +35,9 @@ export const integrationCatalog = new IntegrationCatalog({
plain,
resend,
slack,
typeform,
stripe,
supabaseManagement,
supabase,
sendgrid,
typeform,
});
@@ -109,15 +109,9 @@ export class IntegrationConnectionCreatedService {
});
// We need to start the run again
await workerQueue.enqueue(
"startRun",
{
id: run.id,
},
{
queueName: `job-queue:${run.queue.id}`,
}
);
await workerQueue.enqueue("startRun", {
id: run.id,
});
}
}
}
@@ -0,0 +1,60 @@
import type { Integration } from "../types";
export const sendgrid: Integration = {
identifier: "sendgrid",
name: "SendGrid",
packageName: "@trigger.dev/sendgrid@latest",
authenticationMethods: {
apikey: {
type: "apikey",
help: {
samples: [
{
title: "Creating the client",
code: `
import { SendGrid } from "@trigger.dev/sendgrid";
const sendgrid = new SendGrid({
id: "__SLUG__",
apiKey: process.env.SENDGRID_API_KEY!,
});
`,
},
{
title: "Using the client",
code: `
client.defineJob({
id: "send-sendgrid-email",
name: "Send SendGrid Email",
version: "0.1.0",
trigger: eventTrigger({
name: "send.email",
schema: z.object({
to: z.string(),
subject: z.string(),
text: z.string(),
}),
}),
integrations: {
sendgrid,
},
run: async (payload, io, ctx) => {
await io.sendgrid.sendEmail({
to: payload.to,
from: "Trigger.dev <hello@email.trigger.dev>",
subject: payload.subject,
text: payload.text,
});
},
});
`,
highlight: [
[13, 15],
[17, 22],
],
},
],
},
},
},
};
@@ -10,11 +10,11 @@ const supabase = new SupabaseManagement({
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
id: "on-new-todos",
name: "On New Todos",
version: "0.1.1",
trigger: supabase.onInsert({
table: "users",
trigger: supabase.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {
},
@@ -33,11 +33,11 @@ const supabase = new SupabaseManagement({
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
id: "on-new-todos",
name: "On New Todos",
version: "0.1.1",
trigger: supabase.onInsert({
table: "users",
trigger: supabase.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {
},
@@ -0,0 +1,45 @@
import type { Job } from "@trigger.dev/database";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { telemetry } from "../telemetry.server";
export class DeleteJobService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(job: Job) {
// Make sure that all the latest versions are disabled
const latestVersions = await this.#prismaClient.jobAlias.findMany({
where: {
jobId: job.id,
name: "latest",
},
include: {
version: true,
},
});
const allDisabled = latestVersions.every((alias) => alias.version.status === "DISABLED");
if (!allDisabled) {
throw new Error("All latest versions must be disabled before deleting a job");
}
// Okay now we need to delete a job by setting the deletedAt field and enqueuing a job to cleanup the job
await this.#prismaClient.job.update({
where: {
id: job.id,
},
data: {
deletedAt: new Date(),
},
});
telemetry.project.deletedJob({
job,
});
}
}
@@ -0,0 +1,103 @@
import type { JobVersion } from "@trigger.dev/database";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
import type { AuthenticatedEnvironment } from "../apiAuth.server";
import { DisableScheduleSourceService } from "../schedules/disableScheduleSource.server";
export type DisableJobServiceOptions = {
slug: string;
version: string;
};
export class DisableJobService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
endpointIdOrEndpoint: string | ExtendedEndpoint,
options: DisableJobServiceOptions
) {
const endpoint =
typeof endpointIdOrEndpoint === "string"
? await findEndpoint(endpointIdOrEndpoint)
: endpointIdOrEndpoint;
return this.#disableJob(endpoint.environment, options);
}
async #disableJob(
environment: AuthenticatedEnvironment,
options: DisableJobServiceOptions
): Promise<JobVersion | undefined> {
// Find the job
const job = await this.#prismaClient.job.findUnique({
where: {
projectId_slug: {
projectId: environment.projectId,
slug: options.slug,
},
},
});
if (!job) {
return;
}
const jobVersion = await this.#prismaClient.jobVersion.findUnique({
where: {
jobId_version_environmentId: {
jobId: job.id,
version: options.version,
environmentId: environment.id,
},
},
});
if (!jobVersion) {
return;
}
if (jobVersion.status === "DISABLED") {
return;
}
// Upsert the JobVersion
const updatedJobVersion = await this.#prismaClient.jobVersion.update({
where: {
id: jobVersion.id,
},
data: {
status: "DISABLED",
},
});
await this.#disableEventDispatcher(updatedJobVersion);
return updatedJobVersion;
}
async #disableEventDispatcher(jobVersion: JobVersion) {
const eventDispatcher = await this.#prismaClient.eventDispatcher.update({
where: {
dispatchableId_environmentId: {
dispatchableId: jobVersion.jobId,
environmentId: jobVersion.environmentId,
},
},
data: {
enabled: false,
},
});
const service = new DisableScheduleSourceService();
await service.call({
key: jobVersion.jobId,
dispatcher: eventDispatcher,
});
}
}
@@ -1,18 +1,18 @@
import type { Endpoint, Integration, Job, JobIntegration, JobVersion } from "@trigger.dev/database";
import {
IntegrationConfig,
JobMetadata,
SCHEDULED_EVENT,
TriggerMetadata,
} from "@trigger.dev/core";
import type { Endpoint, Integration, Job, JobIntegration, JobVersion } from "@trigger.dev/database";
import { DEFAULT_MAX_CONCURRENT_RUNS } from "~/consts";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
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;
@@ -34,7 +34,21 @@ export class RegisterJobService {
endpoint: Endpoint,
environment: AuthenticatedEnvironment,
metadata: JobMetadata
): Promise<JobVersion> {
): Promise<JobVersion | undefined> {
// Check the job doesn't already exist and is deleted
const existingJob = await this.#prismaClient.job.findUnique({
where: {
projectId_slug: {
projectId: environment.projectId,
slug: metadata.id,
},
},
});
if (existingJob && existingJob.deletedAt && !metadata.enabled) {
return;
}
const integrations = new Map<string, Integration>();
for (const [, jobIntegration] of Object.entries(metadata.integrations)) {
@@ -155,6 +169,7 @@ export class RegisterJobService {
},
update: {
title: metadata.name,
deletedAt: metadata.enabled ? null : undefined,
},
include: {
integrations: {
@@ -166,13 +181,9 @@ export class RegisterJobService {
});
// Upsert the JobQueue
const queueName =
typeof metadata.queue === "string"
? metadata.queue
: typeof metadata.queue === "object"
? metadata.queue.name
: "default";
const queueName = "default";
// Job Queues are going to be deprecated or used for something else, we're just doing this for now
const jobQueue = await this.#prismaClient.jobQueue.upsert({
where: {
environmentId_name: {
@@ -187,16 +198,10 @@ export class RegisterJobService {
},
},
name: queueName,
maxJobs:
typeof metadata.queue === "object"
? metadata.queue.maxConcurrent || DEFAULT_MAX_CONCURRENT_RUNS
: DEFAULT_MAX_CONCURRENT_RUNS,
maxJobs: DEFAULT_MAX_CONCURRENT_RUNS,
},
update: {
maxJobs:
typeof metadata.queue === "object"
? metadata.queue.maxConcurrent || DEFAULT_MAX_CONCURRENT_RUNS
: DEFAULT_MAX_CONCURRENT_RUNS,
maxJobs: DEFAULT_MAX_CONCURRENT_RUNS,
},
});
@@ -245,10 +250,12 @@ export class RegisterJobService {
version: metadata.version,
eventSpecification,
preprocessRuns: metadata.preprocessRuns,
startPosition: metadata.startPosition === "initial" ? "INITIAL" : "LATEST",
startPosition: "LATEST",
status: "ACTIVE",
},
update: {
startPosition: metadata.startPosition === "initial" ? "INITIAL" : "LATEST",
status: "ACTIVE",
startPosition: "LATEST",
eventSpecification,
preprocessRuns: metadata.preprocessRuns,
queue: {
@@ -411,6 +418,7 @@ export class RegisterJobService {
type: "JOB_VERSION",
id: jobVersion.id,
},
enabled: true,
},
});
@@ -436,7 +444,7 @@ export class RegisterJobService {
},
},
create: {
event: SCHEDULED_EVENT,
event: [SCHEDULED_EVENT],
source: "trigger.dev",
payloadFilter: {},
contextFilter: {},
@@ -454,6 +462,7 @@ export class RegisterJobService {
type: "JOB_VERSION",
id: jobVersion.id,
},
enabled: true,
},
});
+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"],
["examples", "output", "connectionString", "payload"],
sensitiveDataReplacer
);
+2 -2
View File
@@ -1,5 +1,5 @@
import type { User } from "~/models/user.server";
import { analytics } from "./analytics.server";
import { telemetry } from "./telemetry.server";
export async function postAuthentication({
user,
@@ -10,5 +10,5 @@ export async function postAuthentication({
loginMethod: User["authenticationMethod"];
isNewUser: boolean;
}) {
analytics.user.identify({ user, isNewUser });
telemetry.user.identify({ user, isNewUser });
}
@@ -1,5 +1,6 @@
import { PrismaClient, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
import { executionWorker } from "../worker.server";
import { dequeueRunExecutionV2 } from "~/models/jobRunExecution.server";
export class CancelRunService {
#prismaClient: PrismaClient;
@@ -17,21 +18,11 @@ export class CancelRunService {
},
});
const shouldDecrementQueue = run.status === "STARTED" || run.status === "PREPROCESSING";
await tx.jobRun.update({
where: { id: runId },
data: {
status: "CANCELED",
completedAt: new Date(),
queue: shouldDecrementQueue
? {
update: {
jobCount: {
decrement: 1,
},
},
}
: undefined,
},
});
@@ -48,13 +39,7 @@ export class CancelRunService {
},
});
await workerQueue.enqueue(
"startQueuedRuns",
{
id: run.queueId,
},
{ tx }
);
await dequeueRunExecutionV2(run, tx);
});
} catch (error) {
throw error;
@@ -1,6 +1,6 @@
import { EXECUTE_JOB_RETRY_LIMIT } from "~/consts";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { $transaction, Prisma, PrismaClient, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
const RESUMABLE_STATUSES = ["FAILURE", "TIMED_OUT", "ABORTED", "CANCELED"];
@@ -18,7 +18,7 @@ export class ContinueRunService {
const run = await tx.jobRun.findUniqueOrThrow({
where: { id: runId },
include: {
queue: true,
environment: true,
},
});
@@ -26,77 +26,22 @@ export class ContinueRunService {
throw new Error("Run is not resumable");
}
if (run.queue.jobCount >= run.queue.maxJobs) {
await tx.jobRun.update({
where: { id: runId },
data: {
status: "QUEUED",
queuedAt: new Date(),
startedAt: null,
completedAt: null,
output: Prisma.DbNull,
timedOutAt: null,
timedOutReason: null,
},
});
} else {
await tx.jobRun.update({
where: { id: runId },
data: {
status: "STARTED",
queuedAt: null,
startedAt: new Date(),
completedAt: null,
output: Prisma.DbNull,
timedOutAt: null,
timedOutReason: null,
queue: {
update: {
jobCount: {
increment: 1,
},
},
},
},
});
await tx.jobRun.update({
where: { id: runId },
data: {
status: "QUEUED",
queuedAt: new Date(),
startedAt: null,
completedAt: null,
output: Prisma.DbNull,
timedOutAt: null,
timedOutReason: null,
},
});
const execution = await tx.jobRunExecution.create({
data: {
run: {
connect: {
id: runId,
},
},
status: "PENDING",
reason: "EXECUTE_JOB",
retryLimit: EXECUTE_JOB_RETRY_LIMIT,
isRetry: true,
},
});
const job = await workerQueue.enqueue(
"performRunExecution",
{
id: execution.id,
},
{ tx }
);
await tx.jobRunExecution.update({
where: { id: execution.id },
data: {
graphileJobId: job.id,
},
});
await workerQueue.enqueue(
"startQueuedRuns",
{
id: run.queueId,
},
{ tx }
);
}
await enqueueRunExecutionV2(run, tx, {
skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
});
},
{ timeout: 10000 }
);

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