Compare commits

...

150 Commits

Author SHA1 Message Date
Eric Allam ebe78017ef Update pnpm lock file after package release merge 2023-10-13 13:21:52 +01:00
github-actions[bot] b697a19954 chore: Update version for release (#579)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-13 13:21:10 +01:00
Eric Allam 29ca09e42d Fixes #611: intervalTriggers of >10 never starting
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 6s
🚀 Publish Trigger.dev Docker / units (push) Failing after 21s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-10-13 09:38:25 +01:00
Eric Allam 375b788ab0 Add row locking with FOR UPDATE in delete query 2023-10-13 09:23:05 +01:00
Matt Aitken d6b44d13f7 Show magic link errors in the UI, switch to useNavigation hook, and return using the authenticator 2023-10-12 18:15:46 +01:00
Matt Aitken c6784ba7b8 Added logging when the magic link authentication function is called 2023-10-12 18:12:26 +01:00
James Ritchie 6b8b1c7cae Added data-action tracking to all the login buttons 2023-10-12 17:38:37 +01:00
Eric Allam 0c20756624 Remove reporter because of https://github.com/graphile/worker/issues/349 2023-10-12 17:06:06 +01:00
Matt Aitken 3d387d602d Added additional log after a magic link user has done verification 2023-10-12 16:48:51 +01:00
Matt Aitken 5d45dea0e1 Added some logging when sending magic link emails 2023-10-12 16:45:05 +01:00
James Ritchie a25751e805 Added some better messaging to the login pages
- Better login page title and subtitle
- Better title and new ‘having issues’ panel on the magic link pages
2023-10-12 16:16:20 +01:00
D-K-P 659be8cbc1 Added Hacktoberfest info to our readme 2023-10-12 13:41:15 +01:00
Matt Aitken 50e3d9e43a Indexing errors don't get displayed anywhere (#605)
* Added EndpointIndex status. Default is PENDING, existing rows are SUCCESS

* Made it easier to create a migration SQL file

* Created a job-catalog file for misconfigured Jobs that should error when running the CLI

* EndpointIndex data and state are now optional

* The Environments page now shows the status of the last refresh

* Improved the UI about endpoints

* Added EndpointIndex error column

* WIP making the endpoint indexing more robust

* Indexing errors are now surfaced

* Improved the error show it shows the job id

* Use “performEndpointIndexing” when you create your first endpoint from the UI

* Use “performEndpointIndexing” for the recurring endpoint checker

* Staging is now auto-indexed every 10 mins too

* Use “performEndpointIndexing” for the webhook

* Moved the throttling to a util

* Removed instructional comments

* Created a reusable retry system with exponential backoff

* Use p-retry for retrying with backoff

* The CLI gets indexing results and displays errors

* Improved the indexing error messages and display in the console

* Use a pre so the Indexing error is correctly split over multiple lines

* Use a db transaction for webhook that triggers endpoint indexing

* Tidied up imports

* Support older versions of the server

* Improved the comment on the misconfigured job

* Changeset: When indexing user's jobs errors are now stored and displayed
2023-10-11 17:31:19 +01:00
Eric Allam 203a431350 Add stripe and airtable to the integrations catalog 2023-10-11 11:55:35 +01:00
Hemachandar 39a332eff1 feat: add filter for active jobs in the dashboard (#601)
* feat: add filter for active jobs in the dashboard

* use cursor-pointer when hovering Switch label

* remove unused import

* remove unnecessary span

* add flex-gap
2023-10-11 11:24:24 +01:00
Arjun S 498f56de3e Feat: Replaicing react hot toast with Sonner toasts (#600)
* Feat: Replaicing react hot toast with  Sonner toasts

* pnpm lock file fix

* Quickfix
2023-10-11 10:52:10 +01:00
Eric Allam b23696b255 Move to reporting worker stats through logs 2023-10-11 10:43:37 +01:00
Eric Allam f3a7e2aa20 Add APP_ENV to worker reporter email 2023-10-10 23:15:06 +01:00
Eric Allam b737f7af92 Report worker metrics when WORKER_REPORTER_EMAIL env var is set 2023-10-10 23:06:07 +01:00
Eric Allam f54a9ff5fc Fixed broken link in runTask description 2023-10-10 16:12:30 +01:00
Eric Allam 067c0db543 Merge branch 'main' of github.com:triggerdotdev/trigger.dev 2023-10-10 15:47:57 +01:00
Eric Allam 773765f82d Cleanup finished graphile_worker.jobs last run more than 7 days ago 2023-10-10 15:47:53 +01:00
Eric Allam 624072c23b Update release.yml 2023-10-10 15:04:13 +01:00
Eric Allam d37576f814 Update release.yml 2023-10-10 15:03:30 +01:00
Eric Allam 476e2f035f Don’t typecheck the workspace packages inside the webapp 2023-10-10 12:02:01 +01:00
Eric Allam ad71964d30 Upgrade local development Node to v18.18.0 using nvm 2023-10-10 11:53:42 +01:00
Rutam Prita Mishra 975c5f1d3c [TRI-1333] : Upgrade all packages to use Node18 and use fetch instead node-fetch (#581)
* [TRI-1333] : Upgrade all packages to use Node18 and use fetch instead of node-fetch

* Changeset Added

* Update pnpm-lock to compatible pnpm v7.18.1

* Restored pnpm-lock to the last known good point compatible with pnpm v7.18.1

* Update chilled-plants-exercise.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-10-10 11:42:02 +01:00
D-K-P 916ed4e366 Updated features table 2023-10-10 09:50:48 +01:00
Ikko Eltociear Ashimine df4667b7f5 Fix typo in sse.ts (#591)
Uknown -> Unknown
2023-10-09 22:08:53 +01:00
md d663ba8985 Fixed #593 Added Contributors Section to readme.md (#594)
* Fixed #593 Added Contributors Section to readme.md 

Fixed #593 Added Contributors Section to readme.md

* Update README.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-10-09 22:08:32 +01:00
Eric Allam 8c8d1598e3 Reuse common workflow jobs in publish workflow 2023-10-09 18:30:55 +01:00
Eric Allam 646bfd37bd Pushing a commit to the branch so I can open a PR (#592)
* Pushing a commit to the branch so I can open a PR

* Couple improvements to the pr checks workflow
2023-10-09 18:28:49 +01:00
Eric Allam c0d5292b06 Attempt to fix pgadmin error in tests 2023-10-09 18:22:38 +01:00
Eric Allam 8c383bdc69 Add a PR checks github workflow to run typecheck, unit tests, and e2e tests on PRs 2023-10-09 18:19:06 +01:00
Hemachandar 97e63d1222 Event Trigger Example Payload: Fix unsetting icon, and Use fallback icon in case of invalid icon-name. (#576)
* fix: unset event-trigger example icon

* fix: use fallback icon when an invalid icon is used (example payload page)
2023-10-09 17:33:08 +01:00
D-K-P 87ee98aff1 Added NestJS to the quick starts side menu 2023-10-09 17:28:57 +01:00
Hemachandar c070e78d06 feat: allow CLI update to accept a version for trigger-dev package (#536)
* feat: allow CLI update to accept a version for trigger-dev package

* remove console log
2023-10-09 17:15:24 +01:00
Eric Allam 1c4f9ff1ff Add pgAdmin instructions to contributing guide 2023-10-09 17:14:40 +01:00
nicktrn ca031a72e7 Add pgAdmin (#533) 2023-10-09 16:57:28 +01:00
Dan cbc976188e GitHub docs improvements (#568)
* Updated github-triggers and intro

* Edited tasks / triggers and main overview page

* Added links to scopes docs and improved copy

* Added an overview, side bar title and improved the ordering / wording

* Added the underlying Github client section and some formatting updates

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
Co-authored-by: Eric Allam <eallam@icloud.com>
2023-10-09 16:48:38 +01:00
Eric Allam 59a94c710e Fixes #583 by filtering out properties with missing values (#584)
This also improves the retrying behavior of the resend integration by skipping retrying certain client errors
2023-10-09 16:04:32 +01:00
James Ritchie 4cd97c81ea Swapped out the Integration page skeleton state for a simpler message 2023-10-09 15:20:17 +01:00
Matt Aitken d0d5a698ec Added versions to Supabase management docs examples that are missing them 2023-10-09 14:03:17 +01:00
Matt Aitken ee033a1fd4 Onboarding, don’t show undefined if there are no extra params for dev command 2023-10-09 11:21:44 +01:00
D-K-P 2394fcdf35 Added line about Stripe Shell to the Stripe docs 2023-10-09 09:58:20 +01:00
Matt Aitken 6edaffeb7e Try and determine an error some HTTP requests are causing (#586) 2023-10-08 17:20:07 +01:00
Matt Aitken 0558b2c595 feat: sveltekit adaptor (#575)
* Feat/svelte kit support (#467)

* feat: implemented svelte-kit adapter

* feat: implemented the @trigger.dev/svelte package

* scaffolded a sveltekit example project with trigger.dev

* added the convertToStandardRequest function in the sveltekit package

* example sveltekit project with the @trigger.dev/svelte package

* doc: added the manual setup guide for sveltekit

* updated the web app onboard for sveltekit

* formatted the manual setup guide for sveltekit

* added a link to webapp sveltekit onboarding

* added a wait function to the sveltekit example app job

* typo fix

* removed comments from the @trigger.dev/svelte

- commented out the useEventRunDetails

* updated package.json to use internal packages

* added a .env.example file

* updated the example-svelte-app

* updated the svelte-example

* updated the onboarding page to reflect requested changes

* made the manual setup guide more specific

* created a .env.example file for svelte-example

* added import aliases

* updated the tsconfig and svelte.config file

* updated to use uint8Array

* added missing paths in the tsconfig

---------

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

* Moved examples to references

* Tweaked the SveleteKit manual setup guide

* Updated zod, and set the svelte/sveltekit package versions to match other packages

* Added the headers to the response from the SvelteKit api/trigger route

* Changeset for Svelte and SvelteKit packages

* Revert change to rawBody

* Remove core from changeset packages

* Delete svelte .npmrc file

* Build svelte package to build, not build/lib

* Made the snapshot instructions easier to copy

* Delete the svelte package

* Latest lockfile

* Updated the SvelteKit docs

* Attempt to get Svelte to import the package

* Updates for port and host options

* Updated lockfile from main

* Update neat-feet-wait.md

---------

Co-authored-by: Chigala <92148630+Chigala@users.noreply.github.com>
2023-10-06 17:12:52 +01:00
Hemachandar b12bc6042f (regression) fix: remove invalid envFile response field in sendEvent cmd (#577)
* fix: remove invalid envFile response field in sendEvent cmd

* add changeset
2023-10-06 16:55:26 +01:00
Eric Allam a7bc5df754 use the new JobRun.internal flag in usage stats 2023-10-06 16:15:13 +01:00
Hemachandar 1c524c5acc feat: add internal flag to job-run table (#563)
* feat: add internal flag to job-run table

* add backfill SQL

* Update internal backfill to only update internal jobs

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-10-06 15:46:09 +01:00
Eric Allam 65ad98c20d Add docs for yield and brb 2023-10-06 15:13:45 +01:00
Eric Allam 24d892e322 Merges 573 with fix for Dockerfile (#578)
* update prisma to 5.4.1 (#573)

* Get prisma 5.4.1 to work in the Dockerfile

---------

Co-authored-by: Karthik <kartim316@gmail.com>
2023-10-06 15:10:43 +01:00
Hemachandar be9b51131d feat: allow CLI dev to use injected environment variables (#532)
* feat: allow CLI dev to use injected environment variables

* Remove console.log

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-10-06 14:18:58 +01:00
Hemachandar 63a65dabe4 chore: add example for eventTrigger() doc (#541)
* chore: add example for eventTrigger() doc

* add documentation to the options parameter
2023-10-06 13:56:28 +01:00
Matt Aitken 1f3b423c22 Removed testing dependencies from nestjs example 2023-10-06 13:18:47 +01:00
Matt Aitken a69a5019f1 Removed NestJS example project tests 2023-10-06 13:16:40 +01:00
Matt Aitken 5910339bf4 Latest lockfile 2023-10-06 12:56:56 +01:00
github-actions[bot] 4b641b25a6 chore: Update version for release (#571)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-06 12:54:07 +01:00
Matt Aitken a847b492bc NestJS framework suport (from @H4ad) (#574)
* feat: added nestjs package adapter

* Latest lockfile

* Updated Astro setup docs: env import

* Astro docs improvements

* Docs: improved the limitations

* Bumped package versions to 2.1.3

* Updated the Astro docs with SSR notes

* test/368/use vitest instead of jest (#470)

* chore: add vitest dependencies

* refactor: replace jest by vitest

* test: disable broken test

* Update pnpm-lock.yaml

* Update pnpm-lock.yaml

---------

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

* Created Temp for pull req, bugreport & feature req

* A few teaks to the templates

* Added instructions for how to do Changeset snapshots

* Redirect people to discord to ask a question

* Documentation Update: Added <github_username> instead of triggerdotdev to avoid confusion while cloning the repository (#477)

* Added a link to the homepage from the side menu (#479)

* Added 2 new named icons

* New side menu link to the homepage

* Updated lock file

* Removed un-used import

* Renamed the examples dir to references (true examples are in another repo and this was confusing)

* Add a references README

* Fixes broken pnpm lock file

* Improve the Astro manual setup guide

* Fixed cal.com link

* Upgrade to the latest remix (pre v2)

* React status hooks (#493)

* Added stripInternal to SDK tsconfig

* Statuses can now be set from a run, and are stored in the database

* Added the key to the returned status

* Made the test job have an extra step and only pass in some of the options

* client.getRunStatuses() and the corresponding endpoint

* client.getRun() now includes status info

* Fixed circular dependency schema

* Translate null to undefined

* Added the react package to the nextjs-reference tsconfig

* Removed unused OpenAI integration from nextjs-reference project

* New hooks for getting the statuses

* Disabled most of the nextjs-reference jobs

* Updated the hooks UI

* Updated the endpoints to deal with null statuses values

* The hook is working, with an example

* Changeset: “You can create statuses in your Jobs that can then be read using React hooks”

* Changeset config is back to the old changelog style

* WIP on new React hooks guide

* Guide docs for the new hooks

* Added the status hooks to the React hooks guide

* Removed the links to the status hooks reference for now

* Re-ordered the hooks

* Fix for an error in the docs

* Set a default of a blank array for the GetRunSchema

* Fixed dependency

* Revert "Upgrade to the latest remix (pre v2)"

This reverts commit 4edc7112ab.

* Update introduction.mdx (#498)

Changed https://github.com/triggerdotdev/examples/tree/main/resend (which was a 404) to https://github.com/triggerdotdev/examples/tree/main/resend-email-form

* Use the bell icon for the new status Tasks

* Going exponential with `Linear` (#478)

* Unleash GPT magic

* Clean up after GPT

* All the hooks

* Provisional integration catalog entry

* Sample webhook jobs

* Attachments with alpha warnings

* Remove some verbose logs

* Fix IP restrictions

* Remove tunnel

* Revert "Remove tunnel"

This reverts commit c5b69ce6524e3b40c26b66cdc56e087b8576b8c6.

* Resolve event name clashes

* Remove circular dependency

* Use correct payload uuid

* Schema fixes

* Fix webhook event name

* Start to Linearify catalog entry

* Remove todo

* More catalog updates

* Make OAuth work

* Rename webhook helper

* Schema juggling

* More discrimination

* Add Issue SLA event

* Simplify triggers

* Handle rate limits

* Fix Project schema

* Payload examples

* Improve event props

* Remove redundant source metadata

* One type to rule them all

* Recursive WithoutFunctions type

* Linear output serializer

* Some tasks

* Update catalog entry

* Dynamic usage sample

* Bump version

* Remove tunnel

* More tasks

* Add optional skipRetrying on runTask errors

* Fail fast on user errors

* Entity getter tasks

* Another couple of tasks

* Token to apiKey

* Sort tasks

* Add filtered issue SLA triggers

* Add docs

* Type fixes

* Job catalog examples

* Serialization helper docs

* Add changeset

* Refactor webhooks

* Enhance properties

* Pagination helper and docs

* Clean up imports

* Change misc catalog job

---------

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

* Use absolute image paths (#490)

* Update sendevent.mdx

* fix: Fail client-side on invalid Stripe event names (#492)

* Parse event names

* Add changeset

* feat: BYO Auth (#491)

* feat: BYO Auth

Define client-side auth resolvers to be able to supply custom authentication credentials for integrations before a run is performed

- Added new defineAuthResolver
- Update all integrations to support the new auth resolvers
- Strip internal symbols from .d.ts in integrations and trigger-sdk
- Added BYO Auth docs
- Update Dynamic Schedule to support associated account IDs
- Create external accounts just-in-time
- Added Account ID field to test job when there are external auth integrations
- Show Account ID on run dashboard
- Added new Run error state called “Unresolved auth”

* Added changeset

* Remove @internal from TriggerIntegration public methods

* Add void to the result union

* DynamicTriggers now work with the new BYO auth system, and added a bunch of docs and docs changes

* Add additional key material for registering dynamic trigger task

* Add new define* instance methods to the overview

* CLI now supports multiple frameworks (with tests) (#480)

* Early work defining CLI framework support

* WIP moving CLI init logic to the Framework class

* Installing files should now work for Nextjs

* Some fixes

* WIP creating unit tests for Next.js project detection

* Delete old jest config

* Latest lockfile

* Detect use of src directory test

* Tests for detection pages/app directory

* Correct detection of Next.js project

* Renamed test file

* Create install files from template files with replacements. With tests

* Added multiple uses of the same replacement

* Created a test for the install step (it fails right now with JS)

* Removed unused import

* Another test that should pass but currently fails…

* Path alias fixed and now has tests

* New pathAlias function used

* Nextjs page install tests

* Fixed app directory install (with tests)

* Removed e2e CLI test, switched to unit testing strategy instead

* Latest lockfile

* The install files are now actual files that are copied and transformed

* Got the template files working correctly after building

* Next steps are now framework specific

* createFileFromTemplate now works with a path again. Uses mock if specified.

* Renamed apiRoute.js to pagesApiRoute.js

* Simplified pages file generation

* Next.js app API route template

* Next.js App routing support, with common files logic shared

* Dev command now uses framework default values if they exist and aren’t overridden

* Unused import

* pathAlias now works for all frameworks

* Added a test to detect Next from the next.config.js

* WIP on Remix framework support

* Tests for Remix install

* Replaced references to Next.js

* Use a green ✔️ instead of  in the CLI

* Support for multiple hostnames

* Tunneling can now use the hostname and port

* Work on multiple ports

* Improved the error messages. Added some extra pots to Next.js

* Update the Remix templates to have .server in the imports

* Remix updated to use server-runtime instead of node. Node v18+

* Frameworks can specify the watch paths and ignore paths

* Define the watch variables above, so we can easily log them for debugging

* Don’t wait for outdated package checking when running the dev command

* Improved the Remix manual setup guide

* Rewriting docs for quickstart

* Updated the Next.js quickstart

* Remix quick start

* Added a changeset

* Improved the Next.js manual setup

* Tweaked the Linear scopes

* Latest lockfile

* Fix for getPathAlias typecheck failure

* Linear getAll type error (weirdly not in VSCode…) and removed the pagination example that uses the SDK as won’t work with timeouts

* Add BYO auth for oauth options

* Add back in Job.toJSON to fix the testing package

* Removed dynamicTrigger @internal from toJSON

* The CLI now checks for a dev server API key in init and dev commands

* Remix onboarding now uses the CLI init command

* Decouple zod (#500)

Zod Schemas is no longer required for validating/inferring event triggers. We’ve taken inspiration from how domain-functions did it: https://github.com/seasonedcc/domain-functions/pull/114

* chore: Update version for release (#481)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Latest lockfile

* feat: Basic usage dashboard to show run volume (#501)

* New usage dashboard with static data

* Implement org usage dash

* Grab chart data for the last 12 months

* If no org is found just return undefined so a 404 will be shown

* Remove mock data

* Fill in missing months with 0s

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>

* Fixed duplicate end month

* hotfix

* hotfix 2

* Astro CLI support (#506)

* Astro framework CLI support

* Changeset: Added Astro automatic installation

* Fixed the package name – was remix, now astro

* Fixed the export of the example

* Added IPv6 localhost to Astro hostnames

* Updated Astro onboarding to show the CLI init command, instead of manual instructions

* Astro quickstart

* Fix for type in Remix quickstart

* Next.js framework detection allows different config file extensions and “next” devDependency

* Made the dev command port more general so it works with various frameworks

* chore: Update version for release (#508)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Fix for incorrectly named Next.js package in manual setup

* Latest lockfile

* Increased the intervalTrigger max from 1 day to 30 days

* Implement the task output redacting to prevent redacted values from showing in the logs

* Express frameworks docs + CLI (#512)

* Manual setup docs

* Added the onboarding

* The emails package now works with Node > 18

* Added CLi support for Express, including custom init command finished messages

* Need to actually log out the installation complete message…

* Fix for an old Remix reference

* Renamed the page export

* Use resolvedOptions.triggerUrl

* Typo in manual instructions

* chore: Update version for release (#513)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Latest lockfile

* Add the STAGING environment by default

* Fixed sparodically failed run creations

- Use a better way of getting the latest job run number to increment
- Make the CreateRunService transaction more reliable
- Invoke dispatchers in parallel
- No longer swallow prisma errors in $transaction

* Swapped out the Homepage link in the side menu for a link to the Changelog

* Youtube embedded video fits its aspect ratio instead of going full width

* Improved CLI init Next.js middleware detection

* CLI init: adds public key as “TRIGGER_PUBLIC_API_KEY” except for Next which overrides this

* Updated the docs for the React hooks

* chore: Update version for release (#521)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Display framework logos on the onboarding setup pages (#519)

* autofocus the search field on the Job page

* Add some documentation around canceling scheduled events

* Updated outdated lockfile

* Updated docs README.md

* Improves the performance of run resuming (#522)

* Improves the perform of run resuming

When runs resume, we try and make sure that tasks that have already been completed are cached and reused. Worst case scenario the client needs to hit the API server once for a non-cached task that is indeed completed on the server, but this can get pretty expensive when there are a larger number of tasks.

This commit does 2 different things to help:

- noop tasks are no longer “cached” using the cachedTasks strategy, instead their idempotency keys are shoved into a bloom filter and the client tests for their inclusion in the bloom filter before running them (since they don’t have any concept of output, this works)
- Additional cached tasks are lazy loaded when a task is run. This allows us to progressively fetch additional tasks to be cached on the client, which will cut down on cache misses by a decent amount

* Create warm-carrots-float.md

* Make io.yield backwards compat with older platform versions

* Better support old clients connecting to server versions that support lazy loading cached tasks

* Fixed type errors when settings headers with unknown value

* Better yield not support error message

* Rename _version to _serverVersion to be more clear

* Add typed filters to `Linear` getAll helper (#517)

* Fix getAll params type

* Changeset

* Search param types

* Update changeset

* `Replicate` integration and remote callbacks (#507)

* Support tasks with remote callbacks

* Add common integration tsconfig

* Add Replicate integration

* Basic job catalog example

* Integration catalog entry

* Check for callbackUrl during executeTask

* Fix getAll

* Improve JSDoc

* Bump version

* Remove named queue

* Simplify runTask types

* Trust the types

* Fail tasks on timeout

* Callback timeout as param

* Mess with types

* performRunExecutionV1

* Update runTask docs

* Shorten callback task methods

* Fix run method return type

* Image processing jobs

* Replicate docs

* Text output example

* Changeset

* Version bump

* Roll back ugly types

* Remove missing types

* Quicker return when waiting on remote callback

* Remote callback example

* Bump version

* Remove schema parsing

* Only schedule positive callback timeout

* Decrease callback secret length

* Explicit default timeouts

* Import deployments tasks

* JSDoc

* Deployments docs

* Fix runTask examples, mention wrappers

---------

Co-authored-by: Eric Allam <eric@trigger.dev>

* Update pnpm lock file

* Allow blank issues

* chore: Update version for release (#538)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Fix pnpm lock file

* feat: New Test page (#558)

* Improvements to the shortcut keys

* Updated code mirror theme to the new dark mode style

* Removed the old test page help panel copy

* WIP new structure for the test page

* Added some of the side panel options for examples and account ID

* Conditionally hide the side panel components if they’re blank

* Example with selected state working

* Allow selecting the example again to overwrite any edits

* Previous run payloads working

* Select a recent payload if there’s no example

* Created Icon and DetailCell components

* The integrations now use the DetailCell

* Use DetailCell on the integrations page

* Added support for description to DetailCell. With proper variants now

* DateTimeAccurate and formatDateTimeAccurate weren’t displaying correctly.

fractionalSecondDigits isn’t in the types, but is supported for 92.5% of users

* DetailCell now allows label and description to be React components

* Using the DetailCell on the Test page

* Styled the CodeMirror scrollbars to match elsewhere

* Adding copy and clear to the JSONEditor, not working properly yet though

* Removed the copy/clear buttons from the Test route

* Delete the light color theme

* The editor now supports copying/clearing etc

* Made it clear when text is copied

* Changed the learn link to a tertiary button

* Fixed CMD shortcut keys

* Run test button now works with ⌘ Enter

* CodeMirror now allows ⌘Enter to escape the field

* Added JSON linter to the test editor

* Fix for buttons not being aligned correctly

* Copy/clear buttons now aligned

* Made the integration DetailCell text smaller

* The test recent payloads now have colored text to help identify the status

* DetailCell descriptions are dimmer

* Added link to docs for BYOA

* Fix for buttons going full width when they shouldn’t

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>

* chore: update doc title for groups (#539)

* chore: update doc title for groups

* update docs for sdk and integrations

* Add instructions for triggering job runs to the job catalog readme

* Updated the Test docs

* Removed a console.log when a user’s file is changed that CLI dev is listening for

* feat: allow cancelling jobs from trigger-client sdk (#562)

* feat: allow cancelling jobs from trigger-client sdk

* use presenter instead of service for non-mutating logic

* chore: upgrade zod to 3.22.3 (#570)

* chore: upgrade zod to 3.22.3

* add changeset

* Created a changeset with the correct starting package version

* Added headers to NestJS response

* Added some types to the NestJS project

* Added fastify types

* Removed log from CodeMirror

* Added InstallPackages component

* Improved the NestJS onboarding instructions

* Updated the onboarding instructions

* Made all the quickstart framework cards snippets, so the page isn’t a nightmare to edit

* NestJS docs updates

* Added dotenv to the earlier code sample

* Latest lockfile

* Moved the nestjs-example to the reference folder

---------

Co-authored-by: Vinícius Lourenço <contact@viniciusl.com.br>
Co-authored-by: Wesley <100464352+ologbonowiwi@users.noreply.github.com>
Co-authored-by: Vishesh Rawal <92795514+visheshrwl@users.noreply.github.com>
Co-authored-by: Eric Allam <eallam@icloud.com>
Co-authored-by: Aniket Bindhani <aniketbindhani44@gmail.com>
Co-authored-by: James Ritchie <james@trigger.dev>
Co-authored-by: D-K-P <dkp.github@pm.me>
Co-authored-by: Gregory <93215236+gjohnsx@users.noreply.github.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: Eric Allam <eric@trigger.dev>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
Co-authored-by: Hemachandar <132386067+hmacr@users.noreply.github.com>
2023-10-06 12:27:15 +01:00
Eric Allam 0df6410561 Need to properly format the payload JSON
🚀 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-10-06 11:53:12 +01:00
Eric Allam c17faabe9f Stringify event payload and context before serialization
This fixes an issue where an event payload was being serialized through remix-typedjson and was causing issues with incorrect meta keys and so deserialization was failing. See https://github.com/kiliman/remix-typedjson/pull/33 for more
2023-10-06 11:21:14 +01:00
Hemachandar 9a187f9eda chore: upgrade zod to 3.22.3 (#570)
* chore: upgrade zod to 3.22.3

* add changeset
2023-10-05 17:00:33 +01:00
Hemachandar 2e9452ab7c feat: allow cancelling jobs from trigger-client sdk (#562)
* feat: allow cancelling jobs from trigger-client sdk

* use presenter instead of service for non-mutating logic
2023-10-05 16:51:59 +01:00
Matt Aitken 914745f642 Removed a console.log when a user’s file is changed that CLI dev is listening for
🚀 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-10-05 16:49:06 +01:00
Matt Aitken 9189bdf50f Updated the Test docs 2023-10-05 14:42:32 +01:00
Eric Allam 3e5a97c4b5 Add instructions for triggering job runs to the job catalog readme 2023-10-05 12:47:40 +01:00
Hemachandar c0aa6633d2 chore: update doc title for groups (#539)
* chore: update doc title for groups

* update docs for sdk and integrations
2023-10-05 11:54:09 +01:00
Matt Aitken 4e35871861 feat: New Test page (#558)
* Improvements to the shortcut keys

* Updated code mirror theme to the new dark mode style

* Removed the old test page help panel copy

* WIP new structure for the test page

* Added some of the side panel options for examples and account ID

* Conditionally hide the side panel components if they’re blank

* Example with selected state working

* Allow selecting the example again to overwrite any edits

* Previous run payloads working

* Select a recent payload if there’s no example

* Created Icon and DetailCell components

* The integrations now use the DetailCell

* Use DetailCell on the integrations page

* Added support for description to DetailCell. With proper variants now

* DateTimeAccurate and formatDateTimeAccurate weren’t displaying correctly.

fractionalSecondDigits isn’t in the types, but is supported for 92.5% of users

* DetailCell now allows label and description to be React components

* Using the DetailCell on the Test page

* Styled the CodeMirror scrollbars to match elsewhere

* Adding copy and clear to the JSONEditor, not working properly yet though

* Removed the copy/clear buttons from the Test route

* Delete the light color theme

* The editor now supports copying/clearing etc

* Made it clear when text is copied

* Changed the learn link to a tertiary button

* Fixed CMD shortcut keys

* Run test button now works with ⌘ Enter

* CodeMirror now allows ⌘Enter to escape the field

* Added JSON linter to the test editor

* Fix for buttons not being aligned correctly

* Copy/clear buttons now aligned

* Made the integration DetailCell text smaller

* The test recent payloads now have colored text to help identify the status

* DetailCell descriptions are dimmer

* Added link to docs for BYOA

* Fix for buttons going full width when they shouldn’t

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-10-05 10:38:28 +01:00
Eric Allam 90f52de147 Fix pnpm lock file 2023-10-04 14:01:48 +01:00
github-actions[bot] 28b05a82d8 chore: Update version for release (#538)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-04 14:00:20 +01:00
Eric Allam b9ed7e2ced Allow blank issues 2023-10-04 13:21:38 +01:00
Eric Allam 5e651d84af Update pnpm 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-10-04 11:00:55 +01:00
nicktrn 6a992a1995 Replicate integration and remote callbacks (#507)
* Support tasks with remote callbacks

* Add common integration tsconfig

* Add Replicate integration

* Basic job catalog example

* Integration catalog entry

* Check for callbackUrl during executeTask

* Fix getAll

* Improve JSDoc

* Bump version

* Remove named queue

* Simplify runTask types

* Trust the types

* Fail tasks on timeout

* Callback timeout as param

* Mess with types

* performRunExecutionV1

* Update runTask docs

* Shorten callback task methods

* Fix run method return type

* Image processing jobs

* Replicate docs

* Text output example

* Changeset

* Version bump

* Roll back ugly types

* Remove missing types

* Quicker return when waiting on remote callback

* Remote callback example

* Bump version

* Remove schema parsing

* Only schedule positive callback timeout

* Decrease callback secret length

* Explicit default timeouts

* Import deployments tasks

* JSDoc

* Deployments docs

* Fix runTask examples, mention wrappers

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
2023-10-04 10:46:17 +01:00
nicktrn 81e886a1ba Add typed filters to Linear getAll helper (#517)
* Fix getAll params type

* Changeset

* Search param types

* Update changeset
2023-10-03 16:48:21 +01:00
Eric Allam ab9e4a989c Improves the performance of run resuming (#522)
* Improves the perform of run resuming

When runs resume, we try and make sure that tasks that have already been completed are cached and reused. Worst case scenario the client needs to hit the API server once for a non-cached task that is indeed completed on the server, but this can get pretty expensive when there are a larger number of tasks.

This commit does 2 different things to help:

- noop tasks are no longer “cached” using the cachedTasks strategy, instead their idempotency keys are shoved into a bloom filter and the client tests for their inclusion in the bloom filter before running them (since they don’t have any concept of output, this works)
- Additional cached tasks are lazy loaded when a task is run. This allows us to progressively fetch additional tasks to be cached on the client, which will cut down on cache misses by a decent amount

* Create warm-carrots-float.md

* Make io.yield backwards compat with older platform versions

* Better support old clients connecting to server versions that support lazy loading cached tasks

* Fixed type errors when settings headers with unknown value

* Better yield not support error message

* Rename _version to _serverVersion to be more clear
2023-10-03 16:44:18 +01:00
Matt Aitken e350659e24 Updated docs README.md 2023-10-02 14:26:54 +01:00
Eric Allam f888a49555 Updated outdated lockfile 2023-09-30 22:20:34 +01:00
Eric Allam 421c249e50 Add some documentation around canceling scheduled events 2023-09-30 22:18:52 +01:00
James Ritchie a8a6f51387 autofocus the search field on the Job page 2023-09-29 18:23:22 +01:00
James Ritchie 12e73eef22 Display framework logos on the onboarding setup pages (#519) 2023-09-29 16:44:35 +01:00
github-actions[bot] 2e33fcb16b chore: Update version for release (#521)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-29 16:42:53 +01:00
Matt Aitken 5912cdd11c Updated the docs for the React hooks 2023-09-29 15:37:53 +01:00
Matt Aitken cc016b3ae3 CLI init: adds public key as “TRIGGER_PUBLIC_API_KEY” except for Next which overrides this 2023-09-29 15:14:52 +01:00
Matt Aitken 7760e09462 Improved CLI init Next.js middleware detection 2023-09-29 11:26:29 +01:00
James Ritchie 3ca4456c88 Youtube embedded video fits its aspect ratio instead of going full width
🚀 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-09-28 17:37:07 +01:00
James Ritchie 618b7f22da Swapped out the Homepage link in the side menu for a link to the Changelog 2023-09-28 17:27:21 +01:00
Eric Allam a12c7c3b0a Fixed sparodically failed run creations
- Use a better way of getting the latest job run number to increment
- Make the CreateRunService transaction more reliable
- Invoke dispatchers in parallel
- No longer swallow prisma errors in $transaction
2023-09-28 17:08:26 +01:00
Eric Allam a42e94c75f Add the STAGING environment by default
🚀 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-09-27 18:17:19 +01:00
Matt Aitken bc757c8ddb Latest lockfile 2023-09-27 18:12:54 +01:00
github-actions[bot] 6e11ab9183 chore: Update version for release (#513)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-27 18:09:16 +01:00
Matt Aitken 2397fcb640 Express frameworks docs + CLI (#512)
* Manual setup docs

* Added the onboarding

* The emails package now works with Node > 18

* Added CLi support for Express, including custom init command finished messages

* Need to actually log out the installation complete message…

* Fix for an old Remix reference

* Renamed the page export

* Use resolvedOptions.triggerUrl

* Typo in manual instructions
2023-09-27 17:44:50 +01:00
Eric Allam 35d0c2a06f Implement the task output redacting to prevent redacted values from showing in the logs 2023-09-27 16:38:30 +01:00
Matt Aitken 813ec74672 Increased the intervalTrigger max from 1 day to 30 days
🚀 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-09-26 17:23:36 +01:00
Matt Aitken 03db13171a Latest lockfile 2023-09-26 16:46:41 +01:00
Matt Aitken 0ecb5129e8 Fix for incorrectly named Next.js package in manual setup 2023-09-26 12:47:49 +01:00
github-actions[bot] 44cb28c1c4 chore: Update version for release (#508)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-26 11:49:03 +01:00
Matt Aitken 4578f6bd64 Astro CLI support (#506)
* Astro framework CLI support

* Changeset: Added Astro automatic installation

* Fixed the package name – was remix, now astro

* Fixed the export of the example

* Added IPv6 localhost to Astro hostnames

* Updated Astro onboarding to show the CLI init command, instead of manual instructions

* Astro quickstart

* Fix for type in Remix quickstart

* Next.js framework detection allows different config file extensions and “next” devDependency

* Made the dev command port more general so it works with various frameworks
2023-09-26 11:44:35 +01:00
Eric Allam 8b25e57613 hotfix 2 2023-09-24 14:43:02 -07:00
Eric Allam 8fb9ea19a3 hotfix 2023-09-24 14:04:46 -07:00
Eric Allam eb4ca0ce2d Fixed duplicate end month
🚀 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-09-22 17:24:55 -07:00
Eric Allam df24cd5b71 feat: Basic usage dashboard to show run volume (#501)
* New usage dashboard with static data

* Implement org usage dash

* Grab chart data for the last 12 months

* If no org is found just return undefined so a 404 will be shown

* Remove mock data

* Fill in missing months with 0s

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-09-22 17:02:53 -07:00
Matt Aitken 284324031c Latest lockfile 2023-09-22 16:23:21 -07:00
github-actions[bot] 2fdf42d444 chore: Update version for release (#481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-22 16:20:21 -07:00
Eric Allam 50137a6f24 Decouple zod (#500)
🚀 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
Zod Schemas is no longer required for validating/inferring event triggers. We’ve taken inspiration from how domain-functions did it: https://github.com/seasonedcc/domain-functions/pull/114
2023-09-22 14:44:03 -07:00
Matt Aitken 3703f989fa Remix onboarding now uses the CLI init command 2023-09-22 14:39:21 -07:00
Matt Aitken 2a42942b01 The CLI now checks for a dev server API key in init and dev commands 2023-09-22 13:37:59 -07:00
Matt Aitken 7b29a92f6a Removed dynamicTrigger @internal from toJSON 2023-09-22 12:24:07 -07:00
Eric Allam 24465c9a7b Add back in Job.toJSON to fix the testing package 2023-09-22 11:21:37 -07:00
Eric Allam 42022b6ba4 Add BYO auth for oauth options 2023-09-22 10:31:40 -07:00
Matt Aitken dcc807d716 Linear getAll type error (weirdly not in VSCode…) and removed the pagination example that uses the SDK as won’t work with timeouts 2023-09-22 10:23:24 -07:00
Matt Aitken b5e37bfa61 Fix for getPathAlias typecheck failure 2023-09-22 09:59:59 -07:00
Matt Aitken 5681ebc756 Latest lockfile 2023-09-22 09:53:30 -07:00
Matt Aitken 886d6fda36 Tweaked the Linear scopes 2023-09-22 09:51:49 -07:00
Matt Aitken 1a4952720f CLI now supports multiple frameworks (with tests) (#480)
* Early work defining CLI framework support

* WIP moving CLI init logic to the Framework class

* Installing files should now work for Nextjs

* Some fixes

* WIP creating unit tests for Next.js project detection

* Delete old jest config

* Latest lockfile

* Detect use of src directory test

* Tests for detection pages/app directory

* Correct detection of Next.js project

* Renamed test file

* Create install files from template files with replacements. With tests

* Added multiple uses of the same replacement

* Created a test for the install step (it fails right now with JS)

* Removed unused import

* Another test that should pass but currently fails…

* Path alias fixed and now has tests

* New pathAlias function used

* Nextjs page install tests

* Fixed app directory install (with tests)

* Removed e2e CLI test, switched to unit testing strategy instead

* Latest lockfile

* The install files are now actual files that are copied and transformed

* Got the template files working correctly after building

* Next steps are now framework specific

* createFileFromTemplate now works with a path again. Uses mock if specified.

* Renamed apiRoute.js to pagesApiRoute.js

* Simplified pages file generation

* Next.js app API route template

* Next.js App routing support, with common files logic shared

* Dev command now uses framework default values if they exist and aren’t overridden

* Unused import

* pathAlias now works for all frameworks

* Added a test to detect Next from the next.config.js

* WIP on Remix framework support

* Tests for Remix install

* Replaced references to Next.js

* Use a green ✔️ instead of  in the CLI

* Support for multiple hostnames

* Tunneling can now use the hostname and port

* Work on multiple ports

* Improved the error messages. Added some extra pots to Next.js

* Update the Remix templates to have .server in the imports

* Remix updated to use server-runtime instead of node. Node v18+

* Frameworks can specify the watch paths and ignore paths

* Define the watch variables above, so we can easily log them for debugging

* Don’t wait for outdated package checking when running the dev command

* Improved the Remix manual setup guide

* Rewriting docs for quickstart

* Updated the Next.js quickstart

* Remix quick start

* Added a changeset

* Improved the Next.js manual setup
2023-09-22 08:55:04 -07:00
Eric Allam c0dfa8048a feat: BYO Auth (#491)
* feat: BYO Auth

Define client-side auth resolvers to be able to supply custom authentication credentials for integrations before a run is performed

- Added new defineAuthResolver
- Update all integrations to support the new auth resolvers
- Strip internal symbols from .d.ts in integrations and trigger-sdk
- Added BYO Auth docs
- Update Dynamic Schedule to support associated account IDs
- Create external accounts just-in-time
- Added Account ID field to test job when there are external auth integrations
- Show Account ID on run dashboard
- Added new Run error state called “Unresolved auth”

* Added changeset

* Remove @internal from TriggerIntegration public methods

* Add void to the result union

* DynamicTriggers now work with the new BYO auth system, and added a bunch of docs and docs changes

* Add additional key material for registering dynamic trigger task

* Add new define* instance methods to the overview
2023-09-22 08:54:31 -07:00
nicktrn 3e63a7e7a0 fix: Fail client-side on invalid Stripe event names (#492)
* Parse event names

* Add changeset
2023-09-22 08:53:04 -07:00
Eric Allam 4cc690a6ce Update sendevent.mdx 2023-09-22 08:44:14 -07:00
nicktrn 537447e318 Use absolute image paths (#490) 2023-09-21 15:45:40 -07:00
nicktrn 15f17d27e0 Going exponential with Linear (#478)
* Unleash GPT magic

* Clean up after GPT

* All the hooks

* Provisional integration catalog entry

* Sample webhook jobs

* Attachments with alpha warnings

* Remove some verbose logs

* Fix IP restrictions

* Remove tunnel

* Revert "Remove tunnel"

This reverts commit c5b69ce6524e3b40c26b66cdc56e087b8576b8c6.

* Resolve event name clashes

* Remove circular dependency

* Use correct payload uuid

* Schema fixes

* Fix webhook event name

* Start to Linearify catalog entry

* Remove todo

* More catalog updates

* Make OAuth work

* Rename webhook helper

* Schema juggling

* More discrimination

* Add Issue SLA event

* Simplify triggers

* Handle rate limits

* Fix Project schema

* Payload examples

* Improve event props

* Remove redundant source metadata

* One type to rule them all

* Recursive WithoutFunctions type

* Linear output serializer

* Some tasks

* Update catalog entry

* Dynamic usage sample

* Bump version

* Remove tunnel

* More tasks

* Add optional skipRetrying on runTask errors

* Fail fast on user errors

* Entity getter tasks

* Another couple of tasks

* Token to apiKey

* Sort tasks

* Add filtered issue SLA triggers

* Add docs

* Type fixes

* Job catalog examples

* Serialization helper docs

* Add changeset

* Refactor webhooks

* Enhance properties

* Pagination helper and docs

* Clean up imports

* Change misc catalog job

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-09-21 15:14:54 -07:00
Matt Aitken 91fc1e80f3 Use the bell icon for the new status Tasks
🚀 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-09-21 15:01:35 -07:00
Gregory dfe680a390 Update introduction.mdx (#498)
🚀 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
Changed https://github.com/triggerdotdev/examples/tree/main/resend (which was a 404) to https://github.com/triggerdotdev/examples/tree/main/resend-email-form
2023-09-21 14:06:14 -07:00
Matt Aitken f73a2a424b Revert "Upgrade to the latest remix (pre v2)"
This reverts commit 4edc7112ab.
2023-09-21 13:19:03 -07:00
Matt Aitken 6ce87f169a Fixed dependency 2023-09-21 11:35:12 -07:00
Matt Aitken ad14983e21 React status hooks (#493)
* Added stripInternal to SDK tsconfig

* Statuses can now be set from a run, and are stored in the database

* Added the key to the returned status

* Made the test job have an extra step and only pass in some of the options

* client.getRunStatuses() and the corresponding endpoint

* client.getRun() now includes status info

* Fixed circular dependency schema

* Translate null to undefined

* Added the react package to the nextjs-reference tsconfig

* Removed unused OpenAI integration from nextjs-reference project

* New hooks for getting the statuses

* Disabled most of the nextjs-reference jobs

* Updated the hooks UI

* Updated the endpoints to deal with null statuses values

* The hook is working, with an example

* Changeset: “You can create statuses in your Jobs that can then be read using React hooks”

* Changeset config is back to the old changelog style

* WIP on new React hooks guide

* Guide docs for the new hooks

* Added the status hooks to the React hooks guide

* Removed the links to the status hooks reference for now

* Re-ordered the hooks

* Fix for an error in the docs

* Set a default of a blank array for the GetRunSchema
2023-09-21 10:24:37 -07:00
Eric Allam 4edc7112ab Upgrade to the latest remix (pre v2) 2023-09-18 22:20:41 +01:00
D-K-P c2ce707f3d Fixed cal.com link 2023-09-18 13:54:53 +01:00
Eric Allam cd70a29970 Improve the Astro manual setup guide 2023-09-15 17:40:36 +01:00
Eric Allam cd94d8fee2 Fixes broken pnpm lock file 2023-09-15 16:17:53 +01:00
Eric Allam 241e40e3e2 Add a references README 2023-09-15 16:12:50 +01:00
Eric Allam 486ed20ae7 Renamed the examples dir to references (true examples are in another repo and this was confusing) 2023-09-15 16:09:04 +01:00
James Ritchie e5ffc3a3f3 Added a link to the homepage from the side menu (#479)
* Added 2 new named icons

* New side menu link to the homepage

* Updated lock file

* Removed un-used import
2023-09-15 15:11:40 +01:00
Aniket Bindhani 363c74c6da Documentation Update: Added <github_username> instead of triggerdotdev to avoid confusion while cloning the repository (#477) 2023-09-15 12:55:56 +01:00
Eric Allam f98c425186 Redirect people to discord to ask a question 2023-09-15 12:54:54 +01:00
Matt Aitken da10ba907f Added instructions for how to do Changeset snapshots 2023-09-15 12:54:38 +01:00
Eric Allam 5aeef1233b A few teaks to the templates 2023-09-15 12:52:29 +01:00
Vishesh Rawal fb5f4e308f Created Temp for pull req, bugreport & feature req 2023-09-15 12:52:29 +01:00
Wesley 6ccb4fbc63 test/368/use vitest instead of jest (#470)
* chore: add vitest dependencies

* refactor: replace jest by vitest

* test: disable broken test

* Update pnpm-lock.yaml

* Update pnpm-lock.yaml

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-09-15 12:51:25 +01:00
Matt Aitken 88ebf4a1a0 Updated the Astro docs with SSR notes 2023-09-14 15:25:43 +01:00
Matt Aitken 9a0e6412e9 Bumped package versions to 2.1.3 2023-09-14 13:37:02 +01:00
Matt Aitken a68a912d56 Docs: improved the limitations 2023-09-14 11:55:51 +01:00
Matt Aitken dde51d6d25 Astro docs improvements 2023-09-14 11:55:38 +01:00
Matt Aitken dbd094216d Updated Astro setup docs: env import 2023-09-14 11:14:35 +01:00
Matt Aitken dc7f335beb Latest lockfile 2023-09-14 09:22:05 +01:00
github-actions[bot] 454cad69b4 chore: Update version for release (#473)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-13 18:07:22 +01:00
Matt Aitken f86117f92b Some improvements to the docs platforms section 2023-09-13 18:05:34 +01:00
Matt Aitken b4c67984bc Latest lockfile 2023-09-13 17:57:22 +01:00
Chigala 1b4db79885 Feat/astro support (#466)
* updated the @trigger.dev/astro package to support typescript

* set up a example astro project

* docs: manual setup of Trigger.dev in an Astro project

* doc: updated the manual setup docs for astro

* updated the webapp onboarding for astro

* cleaned up the example astro project

* removed next from dependency 😅

* updated the readme

* moved trigger.ts file and job folder to src directory

* created a .env.example file

* updated tsconfig

* updated to use aliases

* updated package.json

* updated the astro onboarding page

* updated the manual installation guide for astro

* updated to use alias

* added astro as  a dev dependency

* Remove extra comma I left in

* Changed Object.create() to a Record, so we have some type safety

* Added the localhost URL to the API in the Astro example

* Added the TRIGGER_API_URL to the trigger client in the example

* Added the CLI dev dependency to the Astro example

* Update the running instructions to be closer to Remix's

* Create friendly-carpets-collect.md

* Fix bad package name for Astro

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-09-13 17:55:33 +01:00
Matt Aitken e1e60499b2 Fix for incorrect remix package name in docs
🚀 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-09-13 15:41:24 +01:00
Matt Aitken 4eb2439d64 Don’t check for the page title in verify jobs e2e test 2023-09-13 13:57:35 +01:00
Matt Aitken 2895ad52fd Re-run the publish workflow when the test have been updated 2023-09-13 13:56:44 +01:00
715 changed files with 33718 additions and 9306 deletions
+1 -6
View File
@@ -1,11 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
"changelog": [
"@remix-run/changelog-github",
{
"repo": "triggerdotdev/trigger.dev"
}
],
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [
[
+13 -4
View File
@@ -1,35 +1,43 @@
*.log
\*.log
.git
.github
# editor
.idea
.vscode
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# next.js
.next/
build
# packages
build
dist
packages/**/dist
packages/\*\*/dist
# misc
.DS_Store
*.pem
\*.pem
.turbo
.vercel
.cache
.output
apps/**/public/build
apps/\*\*/public/build
cypress/screenshots
cypress/videos
@@ -38,6 +46,7 @@ apps/**/styles/tailwind.css
packages/**/styles/tailwind.css
.changeset
references
examples
CHANGESETS.md
CONTRIBUTING.md
+2
View File
@@ -31,6 +31,8 @@ CLOUD_AIRTABLE_CLIENT_ID=
CLOUD_AIRTABLE_CLIENT_SECRET=
CLOUD_GITHUB_CLIENT_ID=
CLOUD_GITHUB_CLIENT_SECRET=
CLOUD_LINEAR_CLIENT_ID=
CLOUD_LINEAR_CLIENT_SECRET=
CLOUD_SLACK_APP_HOST=
CLOUD_SLACK_CLIENT_ID=
CLOUD_SLACK_CLIENT_SECRET=
+38
View File
@@ -0,0 +1,38 @@
name: 🐞 Bug Report
description: Create a bug report to help us improve
title: "bug: "
labels: ["🐞 unconfirmed bug"]
body:
- type: textarea
attributes:
label: Provide environment information
description: |
Run this command in your project root and paste the results:
```bash
npx envinfo --system --binaries
```
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
validations:
required: true
- type: input
attributes:
label: Reproduction repo
description: If applicable, please provide a link to a reproduction repo or a Stackblitz / CodeSandbox project. Your issue may be closed if this is not provided and we are unable to reproduce the issue. If your bug is a docs issue, link the appropriate page.
validations:
required: true
- type: textarea
attributes:
label: To reproduce
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the bug here, screenshots if applicable.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Ask a Question
url: https://trigger.dev/discord
about: Ask questions and discuss with other community members
@@ -0,0 +1,27 @@
name: Feature Request
description: Suggest an idea for this project
title: "feat: "
labels: ["🌟 enhancement"]
body:
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like to see
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternate solutions
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
+12
View File
@@ -0,0 +1,12 @@
"📌 area: cli":
- any: ["cli/**/*"]
"📌 area: t3-app":
- any: ["cli/template/**/*"]
"📚 documentation":
- any: ["www/**/*"]
- any: ["**/*.md"]
"📌 area: ci":
- any: [".github/**/*"]
+27
View File
@@ -0,0 +1,27 @@
Closes #<issue>
## ✅ Checklist
- [ ] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md)
- [ ] The PR title follows the convention.
- [ ] I ran and tested the code works
---
## Testing
_[Describe the steps you took to test this change]_
---
## Changelog
_[Short description of what has changed]_
---
## Screenshots
_[Screenshots]_
💯
+65
View File
@@ -0,0 +1,65 @@
name: "🧪 E2E Tests"
on:
workflow_call:
jobs:
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 ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
# Build packages
pnpm run build --filter @references/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
+31
View File
@@ -0,0 +1,31 @@
name: 🤖 PR Checks
on:
pull_request_target:
branches:
- main
paths-ignore:
- "**.md"
- ".github/CODEOWNERS"
- ".github/ISSUE_TEMPLATE/**"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
jobs:
typecheck:
uses: ./.github/workflows/typecheck.yml
secrets: inherit
units:
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
e2e:
uses: ./.github/workflows/e2e.yml
secrets: inherit
+9 -114
View File
@@ -23,6 +23,7 @@ on:
- "turbo.json"
- "docker/Dockerfile"
- "docker/scripts/**"
- "tests/**"
permissions:
id-token: write
@@ -37,125 +38,19 @@ env:
jobs:
typecheck:
name: ʦ TypeScript
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
uses: ./.github/workflows/typecheck.yml
secrets: inherit
- 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: 📀 Generate Prisma Client
run: pnpm run generate
- 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
units:
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
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
uses: ./.github/workflows/e2e.yml
secrets: inherit
publish:
needs: [typecheck, unitTests, e2e]
needs: [typecheck, units, e2e]
runs-on: buildjet-4vcpu-ubuntu-2204
outputs:
version: ${{ steps.get_version.outputs.version }}
+5 -10
View File
@@ -4,16 +4,11 @@ on:
push:
branches:
- main
paths:
- ".github/workflows/release.yml"
- "packages/**"
- "!packages/**/*.md"
- ".changeset/**"
- "integrations/**"
- "!integrations/**/*.md"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
paths-ignore:
- "**.md"
- ".github/CODEOWNERS"
- ".github/ISSUE_TEMPLATE/**"
jobs:
release:
+32
View File
@@ -0,0 +1,32 @@
name: "ʦ TypeScript"
on:
workflow_call:
jobs:
typecheck:
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: 📀 Generate Prisma Client
run: pnpm run generate
- name: 🔎 Type check
run: pnpm run typecheck --filter webapp
+30
View File
@@ -0,0 +1,30 @@
name: "🧪 Unit Tests"
on:
workflow_call:
jobs:
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
+1 -1
View File
@@ -1 +1 @@
v18.12.1
v18.18.0
+9
View File
@@ -19,6 +19,15 @@
"name": "Chrome webapp",
"url": "http://localhost:3030",
"webRoot": "${workspaceFolder}/apps/webapp/app"
},
{
"type": "node-terminal",
"request": "launch",
"name": "Debug BYO Auth",
"command": "pnpm run byo-auth",
"envFile": "${workspaceFolder}/references/job-catalog/.env",
"cwd": "${workspaceFolder}/references/job-catalog",
"sourceMaps": true
}
]
}
+28
View File
@@ -27,3 +27,31 @@ Please follow the best-practice of adding changesets in the same commit as the c
3. Create version `pnpm run changeset:version`
4. Release `pnpm run changeset:release`
5. Switch back to normal mode by running `pnpm run changeset:normal`
## Snapshot instructions
!MAKE SURE TO UPDATE THE TAG IN THE INSTRUCTIONS BELOW!
1. Add changesets as usual
```sh
pnpm run changeset:add
```
2. Create a snapshot version (replace "prerelease" with your tag)
```sh
pnpm exec changeset version --snapshot prerelease
```
3. Build the packages:
```sh
pnpm run build --filter "@trigger.dev/*"
```
4. Publish the snapshot (replace "dev" with your tag)
```sh
pnpm exec changeset publish --no-git-tag --snapshot --tag prerelease
```
+25 -13
View File
@@ -23,11 +23,11 @@ branch are tagged into a release monthly.
1. Clone the repo into a public GitHub repository or [fork the repo](https://github.com/triggerdotdev/trigger.dev/fork). If you plan to distribute the code, keep the source code public to comply with the [Apache Licence 2.0](https://github.com/triggerdotdev/trigger.dev/blob/main/LICENSE).
```
git clone https://github.com/triggerdotdev/trigger.dev.git
git clone https://github.com/<github_username>/trigger.dev.git
```
> If you are on windows, run the following command on gitbash with admin privileges:
> `git clone -c core.symlinks=true https://github.com/triggerdotdev/trigger.dev.git`
> `git clone -c core.symlinks=true https://github.com/<github_username>/trigger.dev.git`
2. Navigate to the project folder
```
@@ -52,9 +52,13 @@ branch are tagged into a release monthly.
Feel free to update `SESSION_SECRET` and `MAGIC_LINK_SECRET` as well using the same method.
6. Start Docker. This starts the required services like Postgres. If this is your first time using Docker, consider going through this [guide](DOCKER_INSTALLATION.md)
```
pnpm run docker
```
This will also start and run a local instance of [pgAdmin](https://www.pgadmin.org/) on [localhost:5480](http://localhost:5480), preconfigured with email `admin@example.com` and pwd `admin`. Then use `postgres` as the password to the Trigger.dev server.
7. Migrate the database
```
pnpm run db:migrate
@@ -133,10 +137,10 @@ pnpm run dev
2. Open a new Terminal window and run the webapp locally and then create a new project in the dashboard. Copy out the dev API key.
3. Create a new temporary Next.js app in examples directory
3. Create a new temporary Next.js app in references directory
```sh
cd ./examples
cd ./references
pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir --import-alias "@/*"
```
@@ -149,7 +153,7 @@ pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir
}
```
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.
5. Back in the terminal, navigate into the reference, 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 ./test-cli
@@ -179,14 +183,14 @@ To run the end-to-end tests, follow the steps below:
```sh
cp ./.env.example ./.env
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
```
2. Set up dependencies
```sh
# Build packages
pnpm run build --filter @examples/nextjs-test^...
pnpm run build --filter @references/nextjs-test^...
pnpm --filter @trigger.dev/database generate
# Move trigger-cli bin to correct place
@@ -221,11 +225,11 @@ pnpm run db:studio
## Add sample jobs
The [examples/jobs-starter](./examples/jobs-starter/) project defines simple jobs you can get started with.
The [references/job-catalog](./references/job-catalog/) project defines simple jobs you can get started with.
1. `cd` into `examples/jobs-starter`
2. Create a `.env.local` file with the following content,
replacing `[TRIGGER_DEV_API_KEY]` with an actual key:
1. `cd` into `references/job-catalog`
2. Create a `.env` file with the following content,
replacing `<TRIGGER_DEV_API_KEY>` with an actual key:
```env
TRIGGER_API_KEY=[TRIGGER_DEV_API_KEY]
@@ -235,12 +239,20 @@ TRIGGER_API_URL=http://localhost:3030
`TRIGGER_API_URL` is used to configure the URL for your Trigger.dev instance,
where the jobs will be registered.
3. Run the `jobs-starter` app:
3. Run one of the the `job-catalog` files:
```sh
pnpm dev
pnpm run events
```
This will open up a local server using `express` on port 8080. Then in a new terminal window you can run the trigger-cli dev command:
```sh
pnpm run dev:trigger
```
See the [Job Catalog](./references/job-catalog/README.md) file for more.
4. Navigate to your trigger.dev instance ([http://localhost:3030](http://localhost:3030/)), to see the jobs.
You can use the test feature to trigger them.
+19 -1
View File
@@ -14,6 +14,18 @@
</div>
# ✨🎃 Get involved with Hacktoberfest 2023! 🎃✨
All of October we're participating in Hacktoberfest and invite you to join us! We have a bunch of issues labeled `🎃 Hacktoberfest` that are ready for you to work on which will count towards Hacktoberfest. We are also running our own game, earn 💎 points to win swag!
- Check out our [Hacktoberfest landing page](https://trigger.dev/hacktoberfest) for how to participate and win swag.
- Contribute to either our [/trigger.dev](https://github.com/triggerdotdev/trigger.dev/labels/%F0%9F%8E%83%20hacktoberfest) or [/jobs-showcase](https://github.com/triggerdotdev/jobs-showcase/labels/%F0%9F%8E%83%20hacktoberfest) repositories and complete issues marked `🎃 Hacktoberfest` to be eligible for swag.
- Join our [Discord](https://discord.gg/JtBAxBr2m3) and get involved in with the community.
_New to Hacktober? Check out the [Hacktoberfest website](https://hacktoberfest.digitalocean.com/) for more information._
🎃 **Happy Hacking!** 🎃
# About Trigger.dev
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
@@ -62,8 +74,8 @@ Click the links to join the discussions about our upcoming features.
| Dashboard | View every Task in every Run | ✅ |
| Serverless | Long-running Jobs on your serverless backend | ✅ |
| React hooks | Easily update your UI with Job progress | ✅ |
| React frameworks | Support for Remix, Astro, RedwoodJS & more | ✅ |
| [Background tasks](https://github.com/triggerdotdev/trigger.dev/discussions/400) | Offload long or intense Tasks to our infrastructure | 🛠️ |
| [React frameworks](https://github.com/triggerdotdev/trigger.dev/discussions/411) | Support for Remix, Astro, RedwoodJS & more | 🛠️ |
| [Long-running servers](https://github.com/triggerdotdev/trigger.dev/discussions/430) | Run Jobs on your long-running backend | 🛠️ |
| Polling Triggers | Subscribe to changes without webhooks | 🕝 |
| Vercel integration | Easy deploy and preview environment support | 🕝 |
@@ -83,3 +95,9 @@ 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 [development guide](./CONTRIBUTING.md).
## 🙏 to our contributors
<a href="https://github.com/triggerdotdev/trigger.dev/graphs/contributors">
<img src="https://contrib.rocks/image?repo=triggerdotdev/trigger.dev" />
</a>
+40 -28
View File
@@ -44,28 +44,7 @@ export function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey:
);
}
export function RunDevCommand() {
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>
);
}
export function TriggerDevCommand() {
export function RunDevCommand({ extra }: { extra?: string }) {
return (
<ClientTabs defaultValue="npm">
<ClientTabsList>
@@ -77,36 +56,69 @@ export function TriggerDevCommand() {
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`npx @trigger.dev/cli@latest dev`}
value={`npm run dev${extra ? ` ${extra}` : ""}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm dlx @trigger.dev/cli@latest dev`}
value={`pnpm run dev${extra ? ` ${extra}` : ""}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn dlx @trigger.dev/cli@latest dev`}
value={`yarn run dev${extra ? ` ${extra}` : ""}`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
export function TriggerDevStep() {
export function TriggerDevCommand({ extra }: { extra?: 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"
value={`npx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm dlx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn dlx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
export function TriggerDevStep({ extra }: { extra?: string }) {
return (
<>
<Paragraph spacing>
In a <span className="text-amber-400">separate terminal window or tab</span> run:
</Paragraph>
<TriggerDevCommand />
<TriggerDevCommand extra={extra} />
<Paragraph spacing variant="small">
If youre not running on port 3000 you can specify the port by adding{" "}
If youre not running on the default you can specify the port by adding{" "}
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
</Paragraph>
<Paragraph spacing variant="small">
@@ -0,0 +1,44 @@
import {
ClientTabs,
ClientTabsList,
ClientTabsTrigger,
ClientTabsContent,
} from "../primitives/ClientTabs";
import { ClipboardField } from "../primitives/ClipboardField";
type InstallPackagesProps = {
packages: string[];
};
export function InstallPackages({ packages }: InstallPackagesProps) {
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 install ${packages.join(" ")}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm install ${packages.join(" ")}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn add ${packages.join(" ")}`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
+72 -18
View File
@@ -1,11 +1,13 @@
import { json as jsonLang } from "@codemirror/lang-json";
import type { ViewUpdate } from "@codemirror/view";
import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid";
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
import { useCodeMirror } from "@uiw/react-codemirror";
import { useRef, useEffect } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Button } from "../primitives/Buttons";
import { getEditorSetup } from "./codeMirrorSetup";
import { darkTheme } from "./codeMirrorTheme";
import { cn } from "~/utils/cn";
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
defaultValue?: string;
@@ -14,6 +16,8 @@ export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
onChange?: (value: string) => void;
onUpdate?: (update: ViewUpdate) => void;
onBlur?: (code: string) => void;
showCopyButton?: boolean;
showClearButton?: boolean;
}
const languages = {
@@ -38,6 +42,8 @@ export function JSONEditor(opts: JSONEditorProps) {
onBlur,
basicSetup,
autoFocus,
showCopyButton = true,
showClearButton = true,
} = {
...defaultProps,
...opts,
@@ -65,7 +71,8 @@ export function JSONEditor(opts: JSONEditorProps) {
onChange,
onUpdate,
};
const { setContainer, state } = useCodeMirror(settings);
const { setContainer, view } = useCodeMirror(settings);
const [copied, setCopied] = useState(false);
useEffect(() => {
if (editor.current) {
@@ -75,24 +82,71 @@ export function JSONEditor(opts: JSONEditorProps) {
//if the defaultValue changes update the editor
useEffect(() => {
if (state !== undefined) {
state.update({
changes: { from: 0, to: state.doc.length, insert: defaultValue },
if (view !== undefined) {
if (view.state.doc.toString() === defaultValue) return;
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: defaultValue },
});
}
}, [defaultValue, state]);
}, [defaultValue, view]);
const clear = useCallback(() => {
if (view === undefined) return;
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: undefined },
});
onChange?.("");
}, [view]);
const copy = useCallback(() => {
if (view === undefined) return;
navigator.clipboard.writeText(view.state.doc.toString());
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1500);
}, [view]);
return (
<div
className={cn(
"overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
opts.className
)}
ref={editor}
onBlur={() => {
if (!onBlur) return;
onBlur(editor.current?.textContent ?? "");
}}
/>
<div className={cn(opts.className, "relative")}>
<div
className="h-full w-full"
ref={editor}
onBlur={() => {
if (!onBlur) return;
onBlur(editor.current?.textContent ?? "");
}}
/>
<div className="absolute right-3 top-3 flex items-center gap-2">
{showClearButton && (
<Button
type="button"
variant="secondary/small"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
clear();
}}
>
Clear
</Button>
)}
{showCopyButton && (
<Button
type="button"
variant="secondary/small"
LeadingIcon={copied ? CheckIcon : ClipboardIcon}
leadingIconClassName={copied ? "text-green-500 group-hover:text-green-500" : undefined}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
copy();
}}
>
Copy
</Button>
)}
</div>
</div>
);
}
@@ -1,34 +1,19 @@
import {
highlightSpecialChars,
drawSelection,
highlightActiveLine,
dropCursor,
lineNumbers,
highlightActiveLineGutter,
keymap,
} from "@codemirror/view";
import type { Extension } from "@codemirror/state";
import { highlightSelectionMatches } from "@codemirror/search";
import { json as jsonLang } from "@codemirror/lang-json";
import { closeBrackets } from "@codemirror/autocomplete";
import { bracketMatching } from "@codemirror/language";
import { indentWithTab } from "@codemirror/commands";
export function getPreviewSetup(): Array<Extension> {
return [
jsonLang(),
highlightSpecialChars(),
drawSelection(),
dropCursor(),
bracketMatching(),
highlightSelectionMatches(),
lineNumbers(),
];
}
export function getViewerSetup(): Array<Extension> {
return [drawSelection(), dropCursor(), bracketMatching(), lineNumbers()];
}
import { jsonParseLinter } from "@codemirror/lang-json";
import { bracketMatching } from "@codemirror/language";
import { lintGutter, lintKeymap, linter } from "@codemirror/lint";
import { highlightSelectionMatches } from "@codemirror/search";
import { Prec, type Extension } from "@codemirror/state";
import {
drawSelection,
dropCursor,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
lineNumbers,
} from "@codemirror/view";
export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array<Extension> {
const options = [
@@ -36,7 +21,20 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A
dropCursor(),
bracketMatching(),
closeBrackets(),
keymap.of([indentWithTab]),
lintGutter(),
linter(jsonParseLinter()),
Prec.highest(
keymap.of([
{
key: "Mod-Enter",
run: () => {
return true;
},
preventDefault: false,
},
])
),
keymap.of([indentWithTab, ...lintKeymap]),
];
if (showLineNumbers) {
@@ -17,10 +17,15 @@ export function darkTheme(): Extension {
violet = "#c678dd",
darkBackground = "#21252b",
highlightBackground = "rgba(71,85,105,0.2)",
background = "#0f172a",
background = "rgba(11, 16, 24 ,100)",
tooltipBackground = "#353a42",
selection = "rgb(71 85 105)",
cursor = "#528bff";
cursor = "#528bff",
scrollbarTrack = "#0E1521",
scrollbarTrackActive = "#131B2B",
scrollbarThumb = "#293649",
scrollbarThumbActive = "#3C4B62",
scrollbarBg = "#0E1521";
const jsonHeroEditorTheme = EditorView.theme(
{
@@ -94,6 +99,45 @@ export function darkTheme(): Extension {
color: ivory,
},
},
".cm-scroller": {
scrollbarWidth: "thin",
scrollbarColor: `${scrollbarThumb} ${scrollbarTrack}`,
},
".cm-scroller::-webkit-scrollbar": {
display: "block",
width: "8px",
height: "8px",
},
".cm-scroller::-webkit-scrollbar-track": {
backgroundColor: scrollbarTrack,
borderRadius: "0",
},
".cm-scroller::-webkit-scrollbar-track:hover": {
backgroundColor: scrollbarTrackActive,
},
".cm-scroller::-webkit-scrollbar-track:active": {
backgroundColor: scrollbarTrackActive,
},
".cm-scroller::-webkit-scrollbar-thumb": {
backgroundColor: scrollbarThumb,
borderRadius: "0",
},
".cm-scroller::-webkit-scrollbar-thumb:hover": {
backgroundColor: scrollbarThumbActive,
},
".cm-scroller::-webkit-scrollbar-thumb:active": {
backgroundColor: scrollbarThumbActive,
},
".cm-scroller::-webkit-scrollbar-corner": {
backgroundColor: scrollbarBg,
borderRadius: "0",
},
".cm-scroller::-webkit-scrollbar-corner:hover": {
backgroundColor: scrollbarBg,
},
".cm-scroller::-webkit-scrollbar-corner:active": {
backgroundColor: scrollbarBg,
},
},
{ dark: true }
);
@@ -155,157 +199,3 @@ export function darkTheme(): Extension {
return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)];
}
export function lightTheme(): Extension[] {
const stringColor = "text-[#53a053]",
numberColor = "text-[#447bef]",
variableColor = "text-[#a42ea2]",
booleanColor = "text-[#e2574e]",
coral = "text-[#e06c75]",
invalid = "text-[#ffffff]",
ivory = "text-[#abb2bf]",
stone = "text-[#7d8799]",
malibu = "text-[#61afef]",
whiskey = "text-[#d19a66]",
violet = "text-[#c678dd]",
darkBackground = "text-[#21252b]",
highlightBackground = "text-[#D0D0D0]",
background = "text-[#ffffff]",
tooltipBackground = "text-[#353a42]",
selection = "text-[#D0D0D0]",
cursor = "text-[#528bff]";
const jsonHeroEditorTheme = EditorView.theme(
{
"&": {
color: ivory,
backgroundColor: background,
},
".cm-content": {
caretColor: cursor,
fontFamily: "monospace",
fontSize: "14px",
},
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
backgroundColor: selection,
},
".cm-panels": { backgroundColor: darkBackground, color: ivory },
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
".cm-searchMatch": {
backgroundColor: "#72a1ff59",
outline: "1px solid #457dff",
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: "#6199ff2f",
},
".cm-activeLine": { backgroundColor: highlightBackground },
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
backgroundColor: "#bad0f847",
outline: "1px solid #515a6b",
},
".cm-gutters": {
backgroundColor: background,
color: stone,
border: "none",
},
".cm-activeLineGutter": {
backgroundColor: highlightBackground,
},
".cm-foldPlaceholder": {
backgroundColor: "transparent",
border: "none",
color: "#ddd",
},
".cm-tooltip": {
border: "none",
backgroundColor: tooltipBackground,
},
".cm-tooltip .cm-tooltip-arrow:before": {
borderTopColor: "transparent",
borderBottomColor: "transparent",
},
".cm-tooltip .cm-tooltip-arrow:after": {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
".cm-tooltip-autocomplete": {
"& > ul > li[aria-selected]": {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: false }
);
/// The highlighting style for code in the JSON Hero theme.
const jsonHeroHighlightStyle = tagHighlighter([
{ tag: tags.keyword, class: violet },
{
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
class: variableColor,
},
{
tag: [tags.function(tags.variableName), tags.labelName],
class: malibu,
},
{
tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
class: whiskey,
},
{ tag: [tags.definition(tags.name), tags.separator], class: ivory },
{
tag: [
tags.typeName,
tags.className,
tags.number,
tags.changed,
tags.annotation,
tags.modifier,
tags.self,
tags.namespace,
],
class: numberColor,
},
{
tag: [
tags.operator,
tags.operatorKeyword,
tags.url,
tags.escape,
tags.regexp,
tags.link,
tags.special(tags.string),
],
class: stringColor,
},
{ tag: [tags.meta, tags.comment], class: stone },
{ tag: tags.link, class: stone },
{ tag: tags.heading, class: coral },
{
tag: [tags.atom, tags.bool, tags.special(tags.variableName)],
class: booleanColor,
},
{
tag: [tags.processingInstruction, tags.string, tags.inserted],
class: stringColor,
},
{ tag: tags.invalid, class: invalid },
]);
return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)];
}
@@ -0,0 +1,74 @@
import { CheckCircleIcon, ClockIcon, XCircleIcon } from "@heroicons/react/20/solid";
import { EndpointIndexStatus } from "@trigger.dev/database";
import { cn } from "~/utils/cn";
import { Spinner } from "../primitives/Spinner";
export function EndpointIndexStatusIcon({ status }: { status: EndpointIndexStatus }) {
switch (status) {
case "PENDING":
return <ClockIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
case "STARTED":
return <Spinner className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
case "SUCCESS":
return (
<CheckCircleIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />
);
case "FAILURE":
return <XCircleIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
}
}
export function EndpointIndexStatusLabel({ status }: { status: EndpointIndexStatus }) {
switch (status) {
case "PENDING":
return (
<span className={endpointIndexStatusClassNameColor(status)}>
{endpointIndexStatusTitle(status)}
</span>
);
case "STARTED":
return (
<span className={endpointIndexStatusClassNameColor(status)}>
{endpointIndexStatusTitle(status)}
</span>
);
case "SUCCESS":
return (
<span className={endpointIndexStatusClassNameColor(status)}>
{endpointIndexStatusTitle(status)}
</span>
);
case "FAILURE":
return (
<span className={endpointIndexStatusClassNameColor(status)}>
{endpointIndexStatusTitle(status)}
</span>
);
}
}
export function endpointIndexStatusTitle(status: EndpointIndexStatus): string {
switch (status) {
case "PENDING":
return "Pending";
case "STARTED":
return "Started";
case "SUCCESS":
return "Success";
case "FAILURE":
return "Failure";
}
}
export function endpointIndexStatusClassNameColor(status: EndpointIndexStatus): string {
switch (status) {
case "PENDING":
return "text-dimmed";
case "STARTED":
return "text-blue-500";
case "SUCCESS":
return "text-green-500";
case "FAILURE":
return "text-rose-500";
}
}
@@ -51,28 +51,28 @@ export function FrameworkSelector() {
<FrameworkLink to={projectSetupNextjsPath(organization, project)} supported>
<NextjsLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupExpressPath(organization, project)}>
<FrameworkLink to={projectSetupExpressPath(organization, project)} supported>
<ExpressLogo className="w-36" />
</FrameworkLink>
<FrameworkLink to={projectSetupRemixPath(organization, project)} supported>
<RemixLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupAstroPath(organization, project)} supported>
<AstroLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
<RedwoodLogo className="w-44" />
</FrameworkLink>
<FrameworkLink to={projectSetupAstroPath(organization, project)}>
<AstroLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
<NuxtLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupSvelteKitPath(organization, project)}>
<FrameworkLink to={projectSetupSvelteKitPath(organization, project)} supported>
<SvelteKitLogo className="w-44" />
</FrameworkLink>
<FrameworkLink to={projectSetupFastifyPath(organization, project)}>
<FastifyLogo className="w-36" />
</FrameworkLink>
<FrameworkLink to={projectSetupNestjsPath(organization, project)}>
<FrameworkLink to={projectSetupNestjsPath(organization, project)} supported>
<NestjsLogo className="w-36" />
</FrameworkLink>
</div>
@@ -79,37 +79,6 @@ export function HowToRunYourJob() {
);
}
export function HowToRunATest() {
return (
<>
<StepNumber
stepNumber="1"
title="Select an environment
"
/>
<StepContentContainer>
<Paragraph spacing>Select the environment youd like the test to run against.</Paragraph>
<img src={selectEnvironment} className="mt-2 w-52" />
</StepContentContainer>
<StepNumber stepNumber="2" title="Write your test payload" />
<StepContentContainer>
<Paragraph spacing>
Write your own payload specific to your Job. Some Triggers also provide example payloads
that you can select from. This will populate the code editor below.
</Paragraph>
<img src={selectExample} className="mt-2 h-40" />
</StepContentContainer>
<StepNumber stepNumber="3" title="Run your test" />
<StepContentContainer>
<Paragraph spacing>When youre happy with the payload, click Run test.</Paragraph>
</StepContentContainer>
<Callout variant="docs" to="https://trigger.dev/docs/documentation/guides/testing-jobs">
Learn more about running tests.
</Callout>
</>
);
}
export function HowToConnectAnIntegration() {
return (
<>
@@ -272,6 +241,21 @@ export function HowToUseApiKeysAndEndpoints() {
you should use the Test feature to trigger any scheduled Jobs.
</Callout>
</StepContentContainer>
<StepNumber
stepNumber="→"
title={
<span className="flex items-center gap-x-2">
<span>Staging</span>
<EnvironmentLabel environment={{ type: "STAGING" }} />
</span>
}
/>
<StepContentContainer>
<Paragraph spacing>
The <InlineCode>STAGING</InlineCode> environment is where your Jobs will run in a staging
environment, meant to mirror your production environment.
</Paragraph>
</StepContentContainer>
<StepNumber
stepNumber="→"
title={
@@ -7,6 +7,7 @@ import { ConnectToOAuthForm } from "./ConnectToOAuthForm";
import { Paragraph } from "../primitives/Paragraph";
import { Client } from "~/presenters/IntegrationsPresenter.server";
import { UpdateOAuthForm } from "./UpdateOAuthForm";
import { LinkButton } from "../primitives/Buttons";
export function SelectOAuthMethod({
integration,
@@ -76,7 +77,7 @@ export function SelectOAuthMethod({
id="EXTERNAL"
value="EXTERNAL"
label="Your users"
description="We will give you OAuth React components so you can connect as your users."
description="Use an external authentication provider or your own user database to provide auth credentails of your users."
variant="description"
/>
</RadioGroup>
@@ -108,14 +109,19 @@ export function SelectOAuthMethod({
)
) : (
<>
<Header2 className="mb-1 mt-4">User OAuth coming soon</Header2>
<Header2 className="mb-1 mt-4">BYO Auth</Header2>
<Paragraph spacing>
End-user OAuth is going to be released soon. If you are interested in being an early
beta tester then please{" "}
<a href="mailto:founders@trigger.dev" className="text-indigo-500 underline">
message us
</a>
.
We support external authentication providers through Auth Resolvers. Read the docs to
learn more:{" "}
<LinkButton
variant="secondary/small"
LeadingIcon={"docs"}
TrailingIcon={"external-link"}
to="https://trigger.dev/docs/documentation/guides/using-integrations-byo-auth"
target="_blank"
>
Bring your own Auth
</LinkButton>
</Paragraph>
</>
))}
@@ -197,6 +197,8 @@ function classForJobStatus(status: JobRunStatus) {
case "TIMED_OUT":
case "WAITING_ON_CONNECTIONS":
case "PENDING":
case "UNRESOLVED_AUTH":
case "INVALID_PAYLOAD":
return "text-rose-500";
default:
return "";
@@ -11,8 +11,8 @@ import {
organizationTeamPath,
projectEnvironmentsPath,
projectIntegrationsPath,
projectSetupPath,
projectPath,
projectSetupPath,
projectTriggersPath,
} from "~/utils/pathBuilder";
import { UserProfilePhoto } from "../UserProfilePhoto";
@@ -96,7 +96,7 @@ export function ProjectSideMenu() {
data-action="environments & api keys"
/>
</div>
<div className="flex flex-col">
<div className="flex flex-col gap-1">
<SideMenuItem
name="Team"
icon="team"
@@ -118,6 +118,14 @@ export function ProjectSideMenu() {
isCollapsed={isCollapsed}
data-action="onboarding"
/>
<SideMenuItem
name="Changelog"
icon="list"
to="https://trigger.dev/changelog"
isCollapsed={isCollapsed}
data-action="changelog"
target="_blank"
/>
<SideMenuItem
name="Account"
icon={UserProfilePhoto}
@@ -146,6 +154,7 @@ function SideMenuItem({
isCollapsed,
forceActive,
hasWarning = false,
target,
}: {
icon: IconNames | React.ComponentType<any>;
name: string;
@@ -153,6 +162,7 @@ function SideMenuItem({
isCollapsed: boolean;
hasWarning?: boolean;
forceActive?: boolean;
target?: string;
}) {
return (
<SimpleTooltip
@@ -164,13 +174,14 @@ function SideMenuItem({
LeadingIcon={icon}
leadingIconClassName="text-dimmed"
to={to}
target={target}
className={({ isActive, isPending }) => {
if (forceActive !== undefined) {
isActive = forceActive;
}
return cn(
"relative",
isActive
isActive || isPending
? "bg-slate-800 text-bright group-hover:bg-slate-800"
: "text-dimmed group-hover:bg-slate-850 group-hover:text-bright"
);
@@ -144,7 +144,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
const textColorClassName = variation.textColor;
return (
<div className={cn(fullWidth ? "flex" : "inline-flex text-xxs", btnClassName, className)}>
<div className={cn("flex", fullWidth ? "" : "w-fit text-xxs", btnClassName, className)}>
<div
className={cn(
textAlignLeft ? "text-left" : "justify-center",
@@ -286,9 +286,13 @@ type NavLinkPropsType = Pick<NavLinkProps, "to" | "target"> &
Omit<React.ComponentProps<typeof ButtonContent>, "className"> & {
className?: (props: { isActive: boolean; isPending: boolean }) => string | undefined;
};
export const NavLinkButton = ({ to, className, ...props }: NavLinkPropsType) => {
export const NavLinkButton = ({ to, className, target, ...props }: NavLinkPropsType) => {
return (
<NavLink to={to} className={cn("group outline-none", props.fullWidth ? "w-full" : "")}>
<NavLink
to={to}
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
target={target}
>
{({ isActive, isPending }) => (
<ButtonContent className={className && className({ isActive, isPending })} {...props} />
)}
@@ -13,6 +13,7 @@ import {
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import { Spinner } from "./Spinner";
export const variantClasses = {
info: {
@@ -51,8 +52,16 @@ export const variantClasses = {
textColor: "text-blue-200",
linkClassName: "transition hover:bg-blue-400/40",
},
pending: {
className: "border-blue-400/20 bg-blue-800/30",
icon: <Spinner className="h-5 w-5 shrink-0 " />,
textColor: "text-blue-300",
linkClassName: "transition hover:bg-blue-400/40",
},
} as const;
export type CalloutVariant = keyof typeof variantClasses;
export function Callout({
children,
className,
@@ -63,7 +72,7 @@ export function Callout({
children?: React.ReactNode;
className?: string;
icon?: React.ReactNode;
variant: keyof typeof variantClasses;
variant: CalloutVariant;
to?: string;
}) {
const variantDefinition = variantClasses[variant];
@@ -63,16 +63,17 @@ export const DateTimeAccurate = ({ date, timeZone = "UTC" }: DateTimeProps) => {
};
function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]): string {
const milliseconds = `00${date.getMilliseconds()}`.slice(-3);
const formattedDateTime = new Intl.DateTimeFormat(locales, {
year: "numeric",
month: "short",
day: "2-digit",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
minute: "numeric",
second: "numeric",
timeZone,
// @ts-ignore this works in 92.5% of browsers https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_datetimeformat_options_parameter_options_fractionalseconddigits_parameter
fractionalSecondDigits: 3,
}).format(date);
return `${formatDateTime}.${milliseconds}`;
return formattedDateTime;
}
@@ -0,0 +1,95 @@
import { cn } from "~/utils/cn";
import { Icon, IconInBox, RenderIcon } from "./Icon";
import { Paragraph } from "./Paragraph";
const variations = {
small: {
label: {
variant: "small" as const,
className: "m-0 leading-[1.1rem]",
},
description: {
variant: "extra-small" as const,
className: "m-0",
},
},
base: {
label: {
variant: "base" as const,
className: "m-0 leading-[1.1rem] ",
},
description: {
variant: "small" as const,
className: "m-0",
},
},
};
type DetailCellProps = {
leadingIcon?: RenderIcon;
leadingIconClassName?: string;
trailingIcon?: RenderIcon;
trailingIconClassName?: string;
label: string | React.ReactNode;
description?: string | React.ReactNode;
className?: string;
variant?: keyof typeof variations;
};
export function DetailCell({
leadingIcon,
leadingIconClassName,
trailingIcon,
trailingIconClassName,
label,
description,
className,
variant = "small",
}: DetailCellProps) {
const variation = variations[variant];
return (
<div
className={cn(
"group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-900",
className
)}
>
<IconInBox
icon={leadingIcon}
className={cn("flex-none transition group-hover:border-slate-750", leadingIconClassName)}
/>
<div className="flex flex-1 flex-col">
<Paragraph
variant={variation.label.variant}
className={cn(
"flex-1 text-left transition group-hover:text-bright",
variation.label.className
)}
>
{label}
</Paragraph>
{description && (
<Paragraph
variant={variation.description.variant}
className={cn(
"flex-1 text-left text-dimmed transition group-hover:text-bright",
variation.description.className
)}
>
{description}
</Paragraph>
)}
</div>
<div className="flex flex-none items-center gap-1">
<Icon
icon={trailingIcon}
className={cn(
"h-6 w-6 flex-none transition group-hover:border-slate-750",
trailingIconClassName
)}
/>
</div>
</div>
);
}
@@ -2,8 +2,17 @@ import type { z } from "zod";
import { Paragraph } from "./Paragraph";
import { NamedIcon } from "./NamedIcon";
import { motion } from "framer-motion";
import { cn } from "~/utils/cn";
export function FormError({ children, id }: { children: React.ReactNode; id?: string }) {
export function FormError({
children,
id,
className,
}: {
children: React.ReactNode;
id?: string;
className?: string;
}) {
return (
<>
{children && (
@@ -11,7 +20,7 @@ export function FormError({ children, id }: { children: React.ReactNode; id?: st
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="flex items-start gap-0.5"
className={cn("flex items-start gap-0.5", className)}
>
<NamedIcon name="error" className="h-4 w-4 shrink-0 justify-start" />
<Paragraph id={id} variant="extra-small" className="text-rose-500">
@@ -0,0 +1,37 @@
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
import { cn } from "~/utils/cn";
export type RenderIcon = IconNamesOrString | React.ComponentType<any>;
type IconProps = {
icon?: RenderIcon;
className?: string;
};
/** Use this icon to either render a passed in React component, or a NamedIcon/CompanyIcon */
export function Icon(props: IconProps) {
if (typeof props.icon === "string") {
return <NamedIcon name={props.icon} className={props.className ?? ""} fallback={<></>} />;
}
const Icon = props.icon;
if (!Icon) {
return <></>;
}
return <Icon className={props.className} />;
}
export function IconInBox({ boxClassName, ...props }: IconProps & { boxClassName?: string }) {
return (
<div
className={cn(
"grid h-9 w-9 place-content-center rounded-sm border border-slate-750 bg-slate-850",
boxClassName
)}
>
<Icon icon={props.icon} className={cn("h-6 w-6", props.className)} />
</div>
);
}
@@ -35,6 +35,7 @@ import {
GlobeAltIcon,
HandRaisedIcon,
HeartIcon,
HomeIcon,
KeyIcon,
LightBulbIcon,
ListBulletIcon,
@@ -50,6 +51,7 @@ import {
UserGroupIcon,
UserIcon,
UserPlusIcon,
WindowIcon,
WrenchScrewdriverIcon,
XCircleIcon,
XMarkIcon,
@@ -74,7 +76,9 @@ const icons = {
"arrow-left": (className: string) => <ArrowLeftIcon className={cn("text-white", className)} />,
background: (className: string) => <CloudIcon className={cn("text-sky-400", className)} />,
beaker: (className: string) => <BeakerIcon className={cn("text-purple-500", className)} />,
bell: (className: string) => <BellAlertIcon className={cn("text-amber-500", className)} />,
billing: (className: string) => <CreditCardIcon className={cn("text-teal-500", className)} />,
browser: (className: string) => <WindowIcon className={cn("text-dimmed", className)} />,
calendar: (className: string) => (
<CalendarDaysIcon className={cn("text-purple-500", className)} />
),
@@ -111,6 +115,7 @@ const icons = {
<HandRaisedIcon className={cn("text-amber-400", className)} />
),
heart: (className: string) => <HeartIcon className={cn("text-rose-500", className)} />,
house: (className: string) => <HomeIcon className={cn("text-dimmed", className)} />,
id: (className: string) => <FingerPrintIcon className={cn("text-rose-200", className)} />,
inactive: (className: string) => <XCircleIcon className={cn("text-rose-500", className)} />,
info: (className: string) => <InformationCircleIcon className={cn("text-blue-500", className)} />,
@@ -126,6 +131,7 @@ const icons = {
"clipboard-checked": (className: string) => (
<ClipboardDocumentCheckIcon className={cn("text-dimmed", className)} />
),
list: (className: string) => <ListBulletIcon className={cn("text-slate-400", className)} />,
log: (className: string) => (
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
),
@@ -23,7 +23,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
const isMac = platform === "mac";
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
const modifiers = relevantShortcut.modifiers ?? [];
const character = relevantShortcut.key;
const character = keyString(relevantShortcut.key, isMac);
return (
<span className={cn(variants[variant], className)}>
@@ -35,6 +35,15 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
);
}
function keyString(key: String, isMac: boolean) {
switch (key) {
case "enter":
return isMac ? "↵" : key;
default:
return key;
}
}
function modifierString(modifier: Modifier, isMac: boolean) {
switch (modifier) {
case "alt":
@@ -42,8 +51,10 @@ function modifierString(modifier: Modifier, isMac: boolean) {
case "ctrl":
return isMac ? "⌃" : "Ctrl+";
case "meta":
return isMac ? "⌘" : "⊞";
return isMac ? "⌘" : "⊞+";
case "shift":
return isMac ? "⇧" : "Shift+";
case "mod":
return isMac ? "⌘" : "Ctrl+";
}
}
@@ -15,7 +15,7 @@ const variations = {
container: "flex items-center gap-x-1.5 rounded hover:bg-slate-850 pr-1 py-[0.1rem] pl-1.5",
root: "h-3 w-6",
thumb: "h-2.5 w-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
text: "text-xs text-slate-400 group-hover:text-slate-200 mt-0.5",
text: "text-xs text-slate-400 group-hover:text-slate-200 hover:cursor-pointer",
},
};
+41 -65
View File
@@ -1,7 +1,7 @@
import { ExclamationCircleIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import { AnimatePresence, motion } from "framer-motion";
import toast, { Toaster, resolveValue, useToasterStore } from "react-hot-toast";
import { Toaster, toast } from "sonner";
import { useTypedLoaderData } from "remix-typedjson";
import { loader } from "~/root";
import { useEffect } from "react";
@@ -11,79 +11,55 @@ const permanentToastDuration = 60 * 60 * 24 * 1000;
export function Toast() {
const { toastMessage } = useTypedLoaderData<typeof loader>();
useEffect(() => {
if (!toastMessage) {
return;
}
const { message, type, options } = toastMessage;
switch (type) {
case "success":
toast.success(message, {
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
});
break;
case "error":
toast.error(message, {
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
});
break;
default:
throw new Error(`${type} is not handled`);
}
toast.custom((t) => <ToastUI variant={type} message={message} t={t as string} />, {
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
});
}, [toastMessage]);
return <Toaster />;
}
export function ToastUI({
variant,
message,
t,
toastWidth = 356, // Default width, matches what sonner provides by default
}: {
variant: "error" | "success";
message: string;
t: string;
toastWidth?: string | number;
}) {
return (
<Toaster
position="bottom-right"
toastOptions={{
success: {
icon: <CheckCircleIcon className="h-6 w-6 text-green-600" />,
},
error: {
icon: <ExclamationCircleIcon className="h-6 w-6 text-rose-600" />,
},
<div
className={`self-end rounded-lg border border-slate-750 bg-midnight-900 shadow-md`}
style={{
width: toastWidth,
}}
>
{(t) => (
<AnimatePresence>
<motion.div
className="flex gap-2 rounded-lg border border-slate-750 bg-no-repeat p-4 text-bright shadow-md"
style={{
opacity: t.visible ? 1 : 0,
background:
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
}}
initial={{ opacity: 0, y: 100 }}
animate={t.visible ? "visible" : "hidden"}
variants={{
hidden: {
opacity: 0,
y: 0,
transition: {
duration: 0.15,
ease: "easeInOut",
},
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.3,
ease: "easeInOut",
},
},
}}
>
{t.icon}
{resolveValue(t.message, t)}
<button className="p-1" onClick={() => toast.dismiss(t.id)}>
<XMarkIcon className="h-4 w-4 text-bright" />
</button>
</motion.div>
</AnimatePresence>
)}
</Toaster>
<div
className="flex w-full gap-2 rounded-lg bg-no-repeat p-4 text-bright"
style={{
background:
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
}}
>
{variant === "success" ? (
<CheckCircleIcon className="h-6 w-6 text-green-600" />
) : (
<ExclamationCircleIcon className="h-6 w-6 text-rose-600" />
)}
{message}
<button className="ms-auto p-1" onClick={() => toast.dismiss(t)}>
<XMarkIcon className="h-4 w-4 text-bright" />
</button>
</div>
</div>
);
}
@@ -2,7 +2,7 @@ import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RunStatusIcon, RunStatusLabel } from "~/components/runs/RunStatuses";
import { MatchedRun, useRun } from "~/hooks/useRun";
import { MatchedRun } from "~/hooks/useRun";
import { formatDuration } from "~/utils";
import {
RunPanel,
@@ -12,7 +12,7 @@ import {
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
import { useMemo } from "react";
import { usePathName } from "~/hooks/usePathName";
import { Run } from "~/presenters/RunPresenter.server";
import { ViewRun } from "~/presenters/RunPresenter.server";
import { cancelSchema } from "~/routes/resources.runs.$runId.cancel";
import { schema } from "~/routes/resources.runs.$runId.rerun";
import { formatDuration } from "~/utils";
@@ -59,7 +59,7 @@ import { TaskCard } from "./TaskCard";
import { TaskCardSkeleton } from "./TaskCardSkeleton";
type RunOverviewProps = {
run: Run;
run: ViewRun;
trigger: {
icon: string;
title: string;
@@ -167,7 +167,13 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
<RunPanelHeader icon={trigger.icon} title={trigger.title} />
<RunPanelBody>
<RunPanelProperties
properties={[{ label: "Event name", text: run.event.name }, ...run.properties]}
properties={[{ label: "Event name", text: run.event.name }]
.concat(
run.event.externalAccount
? [{ label: "Account ID", text: run.event.externalAccount.identifier }]
: []
)
.concat(run.properties)}
/>
</RunPanelBody>
</RunPanel>
+2 -2
View File
@@ -3,7 +3,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { Fragment, useState } from "react";
import simplur from "simplur";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Task } from "~/presenters/RunPresenter.server";
import { ViewTask } from "~/presenters/RunPresenter.server";
import { formatDuration } from "~/utils";
import { cn } from "~/utils/cn";
import {
@@ -22,7 +22,7 @@ import {
} from "./RunCard";
import { TaskStatusIcon } from "./TaskStatus";
type TaskCardProps = Task & {
type TaskCardProps = ViewTask & {
selectedId?: string;
selectedTask: (id: string) => void;
isLast: boolean;
@@ -25,7 +25,7 @@ export function TriggerDetail({
};
properties: DisplayProperty[];
}) {
const { id, name, payload, timestamp, deliveredAt } = trigger;
const { id, name, payload, context, timestamp, deliveredAt } = trigger;
return (
<RunPanel selected={false}>
@@ -45,6 +45,14 @@ export function TriggerDetail({
/>
)}
<RunPanelIconProperty icon="id" label="Event name" value={name} />
<RunPanelIconProperty icon="account" label="Event ID" value={id} />
{trigger.externalAccount && (
<RunPanelIconProperty
icon="account"
label="Account ID"
value={trigger.externalAccount.identifier}
/>
)}
</RunPanelIconSection>
<RunPanelDivider />
<div className="mt-4 flex flex-col gap-2">
@@ -55,7 +63,9 @@ export function TriggerDetail({
</div>
)}
<Header3>Payload</Header3>
<CodeBlock code={JSON.stringify(payload, null, 2)} />
<CodeBlock code={payload} />
<Header3>Context</Header3>
<CodeBlock code={context} />
</div>
</RunPanelBody>
</RunPanel>
+30 -16
View File
@@ -1,15 +1,14 @@
import type { JobRunExecution, JobRunStatus } from "@trigger.dev/database";
import { NoSymbolIcon } from "@heroicons/react/20/solid";
import {
CheckCircleIcon,
ClockIcon,
ExclamationTriangleIcon,
StopIcon,
WrenchIcon,
XCircleIcon,
} from "@heroicons/react/24/solid";
import type { JobRunStatus } from "@trigger.dev/database";
import { cn } from "~/utils/cn";
import { Spinner } from "../primitives/Spinner";
import { HandRaisedIcon, NoSymbolIcon } from "@heroicons/react/20/solid";
export function hasFinished(status: JobRunStatus): boolean {
return (
@@ -17,7 +16,9 @@ export function hasFinished(status: JobRunStatus): boolean {
status === "FAILURE" ||
status === "ABORTED" ||
status === "TIMED_OUT" ||
status === "CANCELED"
status === "CANCELED" ||
status === "UNRESOLVED_AUTH" ||
status === "INVALID_PAYLOAD"
);
}
@@ -48,6 +49,9 @@ export function RunStatusIcon({ status, className }: { status: JobRunStatus; cla
return <XCircleIcon className={cn(runStatusClassNameColor(status), className)} />;
case "TIMED_OUT":
return <ExclamationTriangleIcon className={cn(runStatusClassNameColor(status), className)} />;
case "UNRESOLVED_AUTH":
case "INVALID_PAYLOAD":
return <XCircleIcon className={cn(runStatusClassNameColor(status), className)} />;
case "WAITING_ON_CONNECTIONS":
return <WrenchIcon className={cn(runStatusClassNameColor(status), className)} />;
case "ABORTED":
@@ -63,26 +67,26 @@ export type RunBasicStatus = "WAITING" | "PENDING" | "RUNNING" | "COMPLETED" | "
export function runBasicStatus(status: JobRunStatus): RunBasicStatus {
switch (status) {
case "SUCCESS":
return "COMPLETED";
case "WAITING_ON_CONNECTIONS":
case "QUEUED":
case "PREPROCESSING":
case "PENDING":
return "PENDING";
case "STARTED":
return "RUNNING";
case "QUEUED":
return "PENDING";
case "FAILURE":
return "FAILED";
case "TIMED_OUT":
return "FAILED";
case "WAITING_ON_CONNECTIONS":
return "PENDING";
case "ABORTED":
return "FAILED";
case "PREPROCESSING":
return "PENDING";
case "UNRESOLVED_AUTH":
case "CANCELED":
case "ABORTED":
case "INVALID_PAYLOAD":
return "FAILED";
case "SUCCESS":
return "COMPLETED";
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
}
}
}
@@ -108,6 +112,14 @@ export function runStatusTitle(status: JobRunStatus): string {
return "Preprocessing";
case "CANCELED":
return "Canceled";
case "UNRESOLVED_AUTH":
return "Unresolved auth";
case "INVALID_PAYLOAD":
return "Invalid payload";
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
}
}
}
@@ -122,6 +134,8 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
case "QUEUED":
return "text-amber-300";
case "FAILURE":
case "UNRESOLVED_AUTH":
case "INVALID_PAYLOAD":
return "text-rose-500";
case "TIMED_OUT":
return "text-amber-300";
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from "@storybook/react";
import { DetailCell } from "../primitives/DetailCell";
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
import { DateTime, DateTimeAccurate } from "../primitives/DateTime";
const meta: Meta = {
title: "Primitives/DetailCells",
};
export default meta;
type Story = StoryObj<typeof Examples>;
export const Basic: Story = {
render: () => <Examples />,
};
function Examples() {
return (
<div className="flex max-w-xl flex-col items-start gap-y-8 p-8">
<DetailCell
leadingIcon="integration"
leadingIconClassName="text-dimmed"
label="Learn how to create your own API Integrations"
variant="base"
trailingIcon="external-link"
trailingIconClassName="text-slate-700 group-hover:text-bright"
/>
<DetailCell
leadingIcon={CodeBracketIcon}
leadingIconClassName="text-blue-500"
label="Issue comment created"
trailingIcon="check"
trailingIconClassName="text-green-500 group-hover:text-green-400"
/>
<DetailCell
leadingIcon={ClockIcon}
leadingIconClassName="text-slate-400"
label={<DateTime date={new Date()} />}
description="Run #42 complete"
trailingIcon="plus"
trailingIconClassName="text-slate-500 group-hover:text-bright"
/>
</div>
);
}
@@ -24,6 +24,8 @@ const shortcuts: ShortcutDefinition[] = [
{ key: "f", modifiers: ["meta"] },
{ key: "k", modifiers: ["meta"] },
{ key: "del", modifiers: ["ctrl", "alt"] },
{ key: "enter", modifiers: ["meta"] },
{ key: "enter", modifiers: ["mod"] },
];
function Collection() {
@@ -67,6 +69,9 @@ function Set({ platform }: { platform: "mac" | "windows" }) {
<Button variant="danger/medium" shortcut={shortcut}>
Danger medium
</Button>
<Button variant="danger/medium" shortcut={shortcut}>
Danger medium
</Button>
</div>
))}
</OperatingSystemContextProvider>
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster, toast } from "sonner";
import { ToastUI } from "../primitives/Toast";
import { Button } from "../primitives/Buttons";
const meta: Meta = {
title: "Primitives/Toast",
};
export default meta;
type Story = StoryObj<typeof Collection>;
export const Toasts: Story = {
render: () => <Collection />,
};
function Collection() {
return (
<div className="flex flex-col items-start gap-y-4 p-4">
<ToastUI variant="success" message="Success UI" t="-" />
<ToastUI variant="error" message="Error UI" t="-" />
<br />
<Button
variant="primary/large"
onClick={() =>
toast.custom((t) => <ToastUI variant="success" message="Success" t={t as string} />, {
duration: Infinity, // Prevents auto-dismissal for demo purposes
})
}
>
Success
</Button>
<Button
variant="primary/large"
onClick={() =>
toast.custom((t) => <ToastUI variant="error" message="Error" t={t as string} />, {
duration: Infinity,
})
}
>
Error
</Button>
<Toaster />
</div>
);
}
+1
View File
@@ -5,3 +5,4 @@ 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;
export const MAX_RUN_YIELDED_EXECUTIONS = 100;
+7 -5
View File
@@ -31,7 +31,7 @@ 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;
swallowPrismaErrors?: boolean;
};
export async function $transaction<R>(
@@ -55,11 +55,9 @@ export async function $transaction<R>(
name: error.name,
});
if (options?.rethrowPrismaErrors) {
throw error;
if (options?.swallowPrismaErrors) {
return;
}
return;
}
throw error;
@@ -124,6 +122,10 @@ function getClient() {
emit: "stdout",
level: "warn",
},
// {
// emit: "stdout",
// level: "query",
// },
],
});
+21 -3
View File
@@ -1,9 +1,21 @@
import { ProjectJob } from "./useJobs";
import { useTextFilter } from "./useTextFilter";
import { useToggleFilter } from "./useToggleFilter";
export function useFilterJobs(jobs: ProjectJob[]) {
const { filterText, setFilterText, filteredItems } = useTextFilter<ProjectJob>({
export function useFilterJobs(jobs: ProjectJob[], onlyActiveJobs = false) {
const toggleFilterRes = useToggleFilter<ProjectJob>({
items: jobs,
filter: (job, onlyActiveJobs) => {
if (onlyActiveJobs && job.status !== "ACTIVE") {
return false;
}
return true;
},
defaultValue: onlyActiveJobs,
});
const textFilterRes = useTextFilter<ProjectJob>({
items: toggleFilterRes.filteredItems,
filter: (job, text) => {
if (job.slug.toLowerCase().includes(text.toLowerCase())) return true;
if (job.title.toLowerCase().includes(text.toLowerCase())) return true;
@@ -24,5 +36,11 @@ export function useFilterJobs(jobs: ProjectJob[]) {
},
});
return { filterText, setFilterText, filteredItems };
return {
filteredItems: textFilterRes.filteredItems,
filterText: textFilterRes.filterText,
setFilterText: textFilterRes.setFilterText,
onlyActiveJobs: toggleFilterRes.isToggleActive,
setOnlyActiveJobs: toggleFilterRes.setToggleActive,
};
}
+23 -11
View File
@@ -1,12 +1,12 @@
import { useEffect, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider";
export type Modifier = "alt" | "ctrl" | "meta" | "shift";
export type Modifier = "alt" | "ctrl" | "meta" | "shift" | "mod";
export type Shortcut = {
key: string;
modifiers?: Modifier[];
enabledOnInputElements?: boolean;
};
export type ShortcutDefinition =
@@ -20,19 +20,31 @@ type useShortcutKeysProps = {
shortcut: ShortcutDefinition;
action: (event: KeyboardEvent) => void;
disabled?: boolean;
enabledOnInputElements?: boolean;
};
export function useShortcutKeys({ shortcut, action, disabled = false }: useShortcutKeysProps) {
const keys = createKeysFromShortcut(shortcut);
useHotkeys(keys, action, { enabled: !disabled });
}
function createKeysFromShortcut(shortcut: ShortcutDefinition) {
const { platform } = useOperatingSystem();
const isMac = platform === "mac";
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
const modifiers = relevantShortcut.modifiers;
const character = relevantShortcut.key;
const relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
return modifiers ? modifiers.map((k) => k).join("+") + "+" : "" + character;
const keys = createKeysFromShortcut(relevantShortcut);
useHotkeys(
keys,
(event, hotkeysEvent) => {
action(event);
},
{
enabled: !disabled,
enableOnFormTags: relevantShortcut.enabledOnInputElements,
enableOnContentEditable: relevantShortcut.enabledOnInputElements,
}
);
}
function createKeysFromShortcut(shortcut: Shortcut) {
const modifiers = shortcut.modifiers;
const character = shortcut.key;
return modifiers ? modifiers.map((k) => k).join("+") + "+" + character : character;
}
+21
View File
@@ -0,0 +1,21 @@
import { useMemo, useState } from "react";
type ToggleFilterProps<T> = {
items: T[];
filter: (item: T, isToggleActive: boolean) => boolean;
defaultValue?: boolean;
};
export function useToggleFilter<T>({ items, filter, defaultValue = false }: ToggleFilterProps<T>) {
const [isToggleActive, setToggleActive] = useState(defaultValue);
const filteredItems = useMemo<T[]>(() => {
return items.filter((item) => filter(item, isToggleActive));
}, [items, isToggleActive]);
return {
isToggleActive,
setToggleActive,
filteredItems,
};
}
@@ -1,14 +0,0 @@
import { z } from "zod";
const IndexEndpointStatsSchema = z.object({
jobs: z.number(),
sources: z.number(),
dynamicTriggers: z.number(),
dynamicSchedules: z.number(),
});
export type IndexEndpointStats = z.infer<typeof IndexEndpointStatsSchema>;
export function parseEndpointIndexStats(stats: unknown): IndexEndpointStats {
return IndexEndpointStatsSchema.parse(stats);
}
@@ -8,7 +8,6 @@ import type {
import { customAlphabet } from "nanoid";
import slug from "slug";
import { prisma, PrismaClientOrTransaction } from "~/db.server";
import { workerQueue } from "~/services/worker.server";
import { createProject } from "./project.server";
export type { Organization };
@@ -76,6 +75,10 @@ export async function createOrganization(
},
attemptCount = 0
): Promise<Organization & { projects: Project[] }> {
if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) {
throw new Error("Organization could not be created.");
}
const uniqueOrgSlug = `${slug(title)}-${nanoid(4)}`;
const orgWithSameSlug = await prisma.organization.findFirst({
@@ -172,10 +175,10 @@ function envSlug(environmentType: RuntimeEnvironment["type"]) {
return "prod";
}
case "STAGING": {
return "staging";
return "stg";
}
case "PREVIEW": {
return "preview";
return "prev";
}
}
}
+1
View File
@@ -65,6 +65,7 @@ export async function createProject(
// Create the dev and prod environments
await createEnvironment(organization, project, "PRODUCTION");
await createEnvironment(organization, project, "STAGING");
for (const member of project.organization.members) {
await createEnvironment(organization, project, "DEVELOPMENT", member);
@@ -16,7 +16,7 @@ export async function resolveRunConnections(
const result: Record<string, ConnectionAuth> = {};
for (const connection of connections) {
if (connection.integration.authSource === "LOCAL") {
if (connection.integration.authSource !== "HOSTED") {
continue;
}
+86 -1
View File
@@ -1,5 +1,5 @@
import type { Task, TaskAttempt } from "@trigger.dev/database";
import { ServerTask } from "@trigger.dev/core";
import { CachedTask, ServerTask } from "@trigger.dev/core";
export type TaskWithAttempts = Task & { attempts: TaskAttempt[] };
@@ -23,5 +23,90 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
attempts: task.attempts.length,
idempotencyKey: task.idempotencyKey,
operation: task.operation,
callbackUrl: task.callbackUrl,
};
}
export type TaskForCaching = Pick<
Task,
"id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId"
>;
export function prepareTasksForCaching(
possibleTasks: TaskForCaching[],
maxSize: number
): {
tasks: CachedTask[];
cursor: string | undefined;
} {
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && !task.noop);
// Select tasks using greedy approach
const tasksToRun: CachedTask[] = [];
let remainingSize = maxSize;
for (const task of tasks) {
const cachedTask = prepareTaskForCaching(task);
const size = calculateCachedTaskSize(cachedTask);
if (size <= remainingSize) {
tasksToRun.push(cachedTask);
remainingSize -= size;
}
}
return {
tasks: tasksToRun,
cursor: tasks.length > tasksToRun.length ? tasks[tasksToRun.length].id : undefined,
};
}
export function prepareTasksForCachingLegacy(
possibleTasks: TaskForCaching[],
maxSize: number
): {
tasks: CachedTask[];
cursor: string | undefined;
} {
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED");
// Prepare tasks and calculate their sizes
const availableTasks = tasks.map((task) => {
const cachedTask = prepareTaskForCaching(task);
return { task: cachedTask, size: calculateCachedTaskSize(cachedTask) };
});
// Sort tasks in ascending order by size
availableTasks.sort((a, b) => a.size - b.size);
// Select tasks using greedy approach
const tasksToRun: CachedTask[] = [];
let remainingSize = maxSize;
for (const { task, size } of availableTasks) {
if (size <= remainingSize) {
tasksToRun.push(task);
remainingSize -= size;
}
}
return {
tasks: tasksToRun,
cursor: undefined,
};
}
function prepareTaskForCaching(task: TaskForCaching): CachedTask {
return {
id: task.idempotencyKey, // We should eventually move this back to task.id
status: task.status,
idempotencyKey: task.idempotencyKey,
noop: task.noop,
output: task.output as any,
parentId: task.parentId,
};
}
function calculateCachedTaskSize(task: CachedTask): number {
return JSON.stringify(task).length;
}
+158 -1
View File
@@ -14,7 +14,7 @@ import { run as graphileRun, parseCronItems } from "graphile-worker";
import omit from "lodash.omit";
import { z } from "zod";
import { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
import { logger } from "~/services/logger.server";
import { workerLogger as logger } from "~/services/logger.server";
export interface MessageCatalogSchema {
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
@@ -81,6 +81,18 @@ export type ZodWorkerDequeueOptions = {
tx?: PrismaClientOrTransaction;
};
const CLEANUP_TASK_NAME = "__cleanupOldJobs";
const REPORTER_TASK_NAME = "__reporter";
export type ZodWorkerCleanupOptions = {
frequencyExpression: string; // cron expression
ttl: number;
maxCount: number;
taskOptions?: CronItemOptions;
};
type ZodWorkerReporter = (event: string, properties: Record<string, any>) => Promise<void>;
export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
name: string;
runnerOptions: RunnerOptions;
@@ -88,6 +100,8 @@ export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
schema: TMessageCatalog;
tasks: ZodTasks<TMessageCatalog>;
recurringTasks?: ZodRecurringTasks;
cleanup?: ZodWorkerCleanupOptions;
reporter?: ZodWorkerReporter;
};
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
@@ -98,6 +112,8 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
#tasks: ZodTasks<TMessageCatalog>;
#recurringTasks?: ZodRecurringTasks;
#runner?: GraphileRunner;
#cleanup: ZodWorkerCleanupOptions | undefined;
#reporter?: ZodWorkerReporter;
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
this.#name = options.name;
@@ -106,6 +122,8 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#runnerOptions = options.runnerOptions;
this.#tasks = options.tasks;
this.#recurringTasks = options.recurringTasks;
this.#cleanup = options.cleanup;
this.#reporter = options.reporter;
}
get graphileWorkerSchema() {
@@ -337,12 +355,45 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
taskList[key] = task;
}
if (this.#cleanup) {
const task: Task = (payload, helpers) => {
return this.#handleCleanup(payload, helpers);
};
taskList[CLEANUP_TASK_NAME] = task;
}
if (this.#reporter) {
const task: Task = (payload, helpers) => {
return this.#handleReporter(payload, helpers);
};
taskList[REPORTER_TASK_NAME] = task;
}
return taskList;
}
#createCronItemsFromRecurringTasks() {
const cronItems: CronItem[] = [];
if (this.#cleanup) {
cronItems.push({
pattern: this.#cleanup.frequencyExpression,
identifier: CLEANUP_TASK_NAME,
task: CLEANUP_TASK_NAME,
options: this.#cleanup.taskOptions,
});
}
if (this.#reporter) {
cronItems.push({
pattern: "50 * * * *", // Every hour at 50 minutes past the hour
identifier: REPORTER_TASK_NAME,
task: REPORTER_TASK_NAME,
});
}
if (!this.#recurringTasks) {
return cronItems;
}
@@ -434,6 +485,112 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
}
}
async #handleCleanup(rawPayload: unknown, helpers: JobHelpers): Promise<void> {
if (!this.#cleanup) {
return;
}
const job = helpers.job;
logger.debug("Received cleanup task", {
payload: rawPayload,
job,
});
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
if (!parsedPayload.success) {
throw new Error(
`Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}`
);
}
const payload = parsedPayload.data;
// Add the this.#cleanup.ttl to the payload._cron.ts
const expirationDate = new Date(payload._cron.ts.getTime() - this.#cleanup.ttl);
logger.debug("Cleaning up old jobs", {
expirationDate,
payload,
});
const rawResults = await this.#prisma.$queryRawUnsafe(
`WITH rows AS (SELECT id FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts LIMIT $2 FOR UPDATE) DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE id IN (SELECT id FROM rows) RETURNING id`,
expirationDate,
this.#cleanup.maxCount
);
const results = Array.isArray(rawResults) ? rawResults : [];
logger.debug("Cleaned up old jobs", {
count: results.length,
expirationDate,
payload,
});
if (this.#reporter) {
await this.#reporter("cleanup_stats", {
count: results.length,
expirationDate,
ts: payload._cron.ts,
});
}
}
async #handleReporter(rawPayload: unknown, helpers: JobHelpers): Promise<void> {
if (!this.#reporter) {
return;
}
logger.debug("Received reporter task", {
payload: rawPayload,
});
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
if (!parsedPayload.success) {
throw new Error(
`Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}`
);
}
const payload = parsedPayload.data;
// Subtract an hour from the payload._cron.ts
const startAt = new Date(payload._cron.ts.getTime() - 1000 * 60 * 60);
const schema = z.array(z.object({ count: z.coerce.number() }));
// Count the number of jobs that have been added since the startAt date and before the payload._cron.ts date
const rawAddedResults = await this.#prisma.$queryRawUnsafe(
`SELECT COUNT(*) FROM ${this.graphileWorkerSchema}.jobs WHERE created_at > $1 AND created_at < $2`,
startAt,
payload._cron.ts
);
const addedCountResults = schema.parse(rawAddedResults)[0];
// Count the total number of jobs in the jobs table
const rawTotalResults = await this.#prisma.$queryRawUnsafe(
`SELECT COUNT(*) FROM ${this.graphileWorkerSchema}.jobs`
);
const totalCountResults = schema.parse(rawTotalResults)[0];
logger.debug("Calculated metrics about the jobs table", {
rawAddedResults,
rawTotalResults,
payload,
});
await this.#reporter("queue_metrics", {
addedCount: addedCountResults.count,
totalCount: totalCountResults.count,
ts: payload._cron.ts,
});
}
#logDebug(message: string, args?: any) {
logger.debug(`[worker][${this.#name}] ${message}`, args);
}
@@ -0,0 +1,72 @@
import { Job } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server";
type ApiRunOptions = {
runId: Job["id"];
maxTasks?: number;
taskDetails?: boolean;
subTasks?: boolean;
cursor?: string;
};
export class ApiRunPresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({
runId,
maxTasks = 20,
taskDetails = false,
subTasks = false,
cursor,
}: ApiRunOptions) {
const take = Math.min(maxTasks, 50);
return await prisma.jobRun.findUnique({
where: {
id: runId,
},
select: {
id: true,
status: true,
startedAt: true,
updatedAt: true,
completedAt: true,
environmentId: true,
output: true,
tasks: {
select: {
id: true,
parentId: true,
displayKey: true,
status: true,
name: true,
icon: true,
startedAt: true,
completedAt: true,
params: taskDetails,
output: taskDetails,
},
where: {
parentId: subTasks ? undefined : null,
},
orderBy: {
id: "asc",
},
take: take + 1,
cursor: cursor
? {
id: cursor,
}
: undefined,
},
statuses: {
select: { key: true, label: true, state: true, data: true, history: true },
},
},
});
}
}
@@ -1,20 +1,26 @@
import { PrismaClient, prisma } from "~/db.server";
import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server";
import { Project } from "~/models/project.server";
import { User } from "~/models/user.server";
import {
import type {
Endpoint,
EndpointIndex,
EndpointIndexStatus,
RuntimeEnvironment,
RuntimeEnvironmentType,
} from "../../../../packages/database/src";
import { env } from "~/env.server";
} from "@trigger.dev/database";
import {
EndpointIndexError,
EndpointIndexErrorSchema,
IndexEndpointStats,
parseEndpointIndexStats,
} from "@trigger.dev/core";
export type Client = {
slug: string;
endpoints: {
DEVELOPMENT: ClientEndpoint;
PRODUCTION: ClientEndpoint;
STAGING?: ClientEndpoint;
};
};
@@ -34,9 +40,11 @@ export type ClientEndpoint =
url: string;
indexWebhookPath: string;
latestIndex?: {
status: EndpointIndexStatus;
source: string;
updatedAt: Date;
stats: IndexEndpointStats;
stats?: IndexEndpointStats;
error?: EndpointIndexError;
};
environment: {
id: string;
@@ -81,9 +89,11 @@ export class EnvironmentsPresenter {
indexingHookIdentifier: true,
indexings: {
select: {
status: true,
source: true,
updatedAt: true,
stats: true,
error: true,
},
take: 1,
orderBy: {
@@ -133,6 +143,8 @@ export class EnvironmentsPresenter {
throw new Error("Development environment not found, this should not happen");
}
const stagingEnvironment = filtered.find((environment) => environment.type === "STAGING");
const productionEnvironment = filtered.find(
(environment) => environment.type === "PRODUCTION"
);
@@ -151,6 +163,9 @@ export class EnvironmentsPresenter {
state: "unconfigured",
environment: productionEnvironment,
},
STAGING: stagingEnvironment
? { state: "unconfigured", environment: stagingEnvironment }
: undefined,
},
};
@@ -161,6 +176,16 @@ export class EnvironmentsPresenter {
client.endpoints.DEVELOPMENT = endpointClient(devEndpoint, developmentEnvironment, baseUrl);
}
if (stagingEnvironment) {
const stagingEndpoint = stagingEnvironment.endpoints.find(
(endpoint) => endpoint.slug === slug
);
if (stagingEndpoint) {
client.endpoints.STAGING = endpointClient(stagingEndpoint, stagingEnvironment, baseUrl);
}
}
const prodEndpoint = productionEnvironment.endpoints.find(
(endpoint) => endpoint.slug === slug
);
@@ -199,7 +224,7 @@ const environmentSortOrder: RuntimeEnvironmentType[] = [
function endpointClient(
endpoint: Pick<Endpoint, "id" | "slug" | "url" | "indexingHookIdentifier"> & {
indexings: Pick<EndpointIndex, "source" | "updatedAt" | "stats">[];
indexings: Pick<EndpointIndex, "status" | "source" | "updatedAt" | "stats" | "error">[];
},
environment: Pick<RuntimeEnvironment, "id" | "apiKey" | "type">,
baseUrl: string
@@ -212,9 +237,13 @@ function endpointClient(
indexWebhookPath: `${baseUrl}/api/v1/endpoints/${environment.id}/${endpoint.slug}/index/${endpoint.indexingHookIdentifier}`,
latestIndex: endpoint.indexings[0]
? {
status: endpoint.indexings[0].status,
source: endpoint.indexings[0].source,
updatedAt: endpoint.indexings[0].updatedAt,
stats: parseEndpointIndexStats(endpoint.indexings[0].stats),
error: endpoint.indexings[0].error
? EndpointIndexErrorSchema.parse(endpoint.indexings[0].error)
: undefined,
}
: undefined,
environment: environment,
@@ -120,8 +120,12 @@ export class IntegrationClientPresenter {
icon: integration.definition.icon,
},
authMethod: {
type: integration.authMethod?.type ?? "local",
name: integration.authMethod?.name ?? "Local Auth",
type:
integration.authMethod?.type ?? integration.authSource === "RESOLVER" ? "local" : "local",
name:
integration.authMethod?.name ?? integration.authSource === "RESOLVER"
? "Auth Resolver"
: "Local Auth",
},
help,
};
@@ -125,8 +125,9 @@ export class IntegrationsPresenter {
name: c.definition.name,
},
authMethod: {
type: c.authMethod?.type ?? "local",
name: c.authMethod?.name ?? "Local Only",
type: c.authMethod?.type ?? c.authSource === "RESOLVER" ? "resolver" : "local",
name:
c.authMethod?.name ?? c.authSource === "RESOLVER" ? "Auth Resolver" : "Local Only",
},
authSource: c.authSource,
setupStatus: c.setupStatus,
@@ -0,0 +1,222 @@
import { PrismaClient, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
export class OrgUsagePresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ userId, slug }: { userId: string; slug: string }) {
const organization = await this.#prismaClient.organization.findFirst({
where: {
slug,
members: {
some: {
userId,
},
},
},
});
if (!organization) {
return;
}
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const startOfLastMonth = new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1); // this works for January as well
// Get count of runs since the start of the current month
const runsCount = await this.#prismaClient.jobRun.count({
where: {
organizationId: organization.id,
createdAt: {
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
},
internal: false,
},
});
// Get the count of runs for last month
const runsCountLastMonth = await this.#prismaClient.jobRun.count({
where: {
organizationId: organization.id,
createdAt: {
gte: startOfLastMonth,
lt: startOfMonth,
},
internal: false,
},
});
// Get the count of the runs for the last 6 months, by month. So for example we want the data shape to be:
// [
// { month: "2021-01", count: 10 },
// { month: "2021-02", count: 20 },
// { month: "2021-03", count: 30 },
// { month: "2021-04", count: 40 },
// { month: "2021-05", count: 50 },
// { month: "2021-06", count: 60 },
// ]
// This will be used to generate the chart on the usage page
// Use prisma queryRaw for this since prisma doesn't support grouping by month
const chartDataRaw = await this.#prismaClient.$queryRaw<
{
month: string;
count: number;
}[]
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' AND "internal" = FALSE GROUP BY month ORDER BY month ASC`;
const chartData = chartDataRaw.map((obj) => ({
name: obj.month,
total: Number(obj.count), // Convert BigInt to Number
}));
const totalJobs = await this.#prismaClient.job.count({
where: {
organizationId: organization.id,
internal: false,
},
});
const totalJobsLastMonth = await this.#prismaClient.job.count({
where: {
organizationId: organization.id,
createdAt: {
lt: startOfMonth,
},
deletedAt: null,
internal: false,
},
});
const totalIntegrations = await this.#prismaClient.integration.count({
where: {
organizationId: organization.id,
},
});
const totalIntegrationsLastMonth = await this.#prismaClient.integration.count({
where: {
organizationId: organization.id,
createdAt: {
lt: startOfMonth,
},
},
});
const totalMembers = await this.#prismaClient.orgMember.count({
where: {
organizationId: organization.id,
},
});
const jobs = await this.#prismaClient.job.findMany({
where: {
organizationId: organization.id,
deletedAt: null,
internal: false,
},
select: {
id: true,
slug: true,
_count: {
select: {
runs: {
where: {
createdAt: {
gte: startOfMonth,
},
},
},
},
},
project: {
select: {
id: true,
name: true,
slug: true,
},
},
},
});
const chartDataDisplay = fillInMissingMonthlyData(chartData, 6);
return {
id: organization.id,
runsCount,
runsCountLastMonth,
chartData: chartDataDisplay,
totalJobs,
totalJobsLastMonth,
totalIntegrations,
totalIntegrationsLastMonth,
totalMembers,
jobs,
};
}
}
// This will fill in missing chart data with zeros
// So for example, if data is [{ name: "2021-01", total: 10 }, { name: "2021-03", total: 30 }] and the totalNumberOfMonths is 6
// And the current month is "2021-04", then this function will return:
// [{ name: "2020-11", total: 0 }, { name: "2020-12", total: 0 }, { name: "2021-01", total: 10 }, { name: "2021-02", total: 0 }, { name: "2021-03", total: 30 }, { name: "2021-04", total: 0 }]
function fillInMissingMonthlyData(
data: Array<{ name: string; total: number }>,
totalNumberOfMonths: number
): Array<{ name: string; total: number }> {
const currentMonth = new Date().toISOString().slice(0, 7);
const startMonth = new Date(
new Date(currentMonth).getFullYear(),
new Date(currentMonth).getMonth() - (totalNumberOfMonths - 2),
1
)
.toISOString()
.slice(0, 7);
const months = getMonthsBetween(startMonth, currentMonth);
let completeData = months.map((month) => {
let foundData = data.find((d) => d.name === month);
return foundData ? { ...foundData } : { name: month, total: 0 };
});
return completeData;
}
// Start month will be like 2023-03 and endMonth will be like 2023-10
// The result should be an array of months between these two months, including the start and end month
// So for example, if startMonth is 2023-03 and endMonth is 2023-10, the result should be:
// ["2023-03", "2023-04", "2023-05", "2023-06", "2023-07", "2023-08", "2023-09", "2023-10"]
function getMonthsBetween(startMonth: string, endMonth: string): string[] {
// Initialize result array
const result: string[] = [];
// Parse the year and month from startMonth and endMonth
let [startYear, startMonthNum] = startMonth.split("-").map(Number);
let [endYear, endMonthNum] = endMonth.split("-").map(Number);
// Loop through each month between startMonth and endMonth
for (let year = startYear; year <= endYear; year++) {
let monthStart = year === startYear ? startMonthNum : 1;
let monthEnd = year === endYear ? endMonthNum : 12;
for (let month = monthStart; month <= monthEnd; month++) {
// Format the month into a string and add it to the result array
result.push(`${year}-${String(month).padStart(2, "0")}`);
}
}
return result;
}
function getLastSecondOfMonth(endMonth: string) {
const [year, month] = endMonth.split("-").map(Number);
const nextMonthFirstDay = new Date(year, month, 1);
nextMonthFirstDay.setDate(0);
nextMonthFirstDay.setHours(23, 59, 59);
return nextMonthFirstDay;
}
@@ -13,10 +13,11 @@ type RunOptions = {
userId: string;
};
export type Run = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>;
export type Task = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["tasks"][number];
export type Event = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["event"];
export type ViewRun = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>;
export type ViewTask = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["tasks"][number];
export type ViewEvent = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["event"];
type QueryEvent = NonNullable<Awaited<ReturnType<RunPresenter["query"]>>>["event"];
type QueryTask = NonNullable<Awaited<ReturnType<RunPresenter["query"]>>>["tasks"][number];
export class RunPresenter {
@@ -76,7 +77,7 @@ export class RunPresenter {
type: run.environment.type,
slug: run.environment.slug,
},
event: run.event,
event: this.#prepareEventData(run.event),
tasks,
runConnections: run.runConnections,
missingConnections: run.missingConnections,
@@ -84,6 +85,22 @@ export class RunPresenter {
};
}
#prepareEventData(event: QueryEvent) {
return {
id: event.eventId,
name: event.name,
payload: JSON.stringify(event.payload),
context: JSON.stringify(event.context),
timestamp: event.timestamp,
deliveredAt: event.deliveredAt,
externalAccount: event.externalAccount
? {
identifier: event.externalAccount.identifier,
}
: undefined,
};
}
query({ id, userId }: RunOptions) {
return this.#prismaClient.jobRun.findFirst({
select: {
@@ -110,11 +127,17 @@ export class RunPresenter {
},
event: {
select: {
id: true,
eventId: true,
name: true,
payload: true,
context: true,
timestamp: true,
deliveredAt: true,
externalAccount: {
select: {
identifier: true,
},
},
},
},
tasks: {
@@ -1,6 +1,8 @@
import { RedactSchema } from "@trigger.dev/core";
import { StyleSchema } from "@trigger.dev/core";
import { PrismaClient, prisma } from "~/db.server";
import { mergeProperties } from "~/utils/mergeProperties.server";
import { Redactor } from "~/utils/redactor";
type DetailsProps = {
id: string;
@@ -61,6 +63,7 @@ export class TaskDetailsPresenter {
completedAt: true,
style: true,
parentId: true,
redact: true,
attempts: {
select: {
number: true,
@@ -85,11 +88,32 @@ export class TaskDetailsPresenter {
return {
...task,
output: task.output ? JSON.stringify(task.output, null, 2) : undefined,
redact: undefined,
output: task.output
? JSON.stringify(this.#stringifyOutputWithRedactions(task.output, task.redact), null, 2)
: undefined,
connection: task.runConnection,
params: task.params as Record<string, any>,
properties: mergeProperties(task.properties, task.outputProperties),
style: task.style ? StyleSchema.parse(task.style) : undefined,
};
}
#stringifyOutputWithRedactions(output: any, redact: unknown): any {
if (!output) {
return;
}
const parsedRedact = RedactSchema.safeParse(redact);
if (!parsedRedact.success) {
return output;
}
const paths = parsedRedact.data.paths;
const redactor = new Redactor(paths);
return redactor.redact(output);
}
}
@@ -4,6 +4,7 @@ import { PrismaClient, prisma } from "~/db.server";
import { Job } from "~/models/job.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { EventExample } from "@trigger.dev/core";
export class TestJobPresenter {
#prismaClient: PrismaClient;
@@ -39,6 +40,15 @@ export class TestJobPresenter {
payload: true,
},
},
integrations: {
select: {
integration: {
select: {
authSource: true,
},
},
},
},
},
},
environment: {
@@ -58,14 +68,22 @@ export class TestJobPresenter {
name: "latest",
},
},
_count: {
runs: {
select: {
runs: {
where: {
isTest: true,
id: true,
createdAt: true,
number: true,
status: true,
event: {
select: {
payload: true,
},
},
},
orderBy: {
createdAt: "desc",
},
take: 5,
},
},
where: {
@@ -88,6 +106,15 @@ export class TestJobPresenter {
throw new Error("Job not found");
}
//collect together the examples, we don't care about the environments
const examples = job.aliases.flatMap((alias) =>
alias.version.examples.map((example) => ({
...example,
icon: example.icon ?? undefined,
payload: example.payload ? JSON.stringify(example.payload, exampleReplacer, 2) : undefined,
}))
);
return {
environments: job.aliases.map((alias) => ({
id: alias.environment.id,
@@ -95,12 +122,18 @@ export class TestJobPresenter {
slug: alias.environment.slug,
userId: alias.environment.orgMember?.userId,
versionId: alias.version.id,
examples: alias.version.examples.map((example) => ({
...example,
payload: JSON.stringify(example.payload, exampleReplacer, 2),
})),
hasAuthResolver: alias.version.integrations.some(
(i) => i.integration.authSource === "RESOLVER"
),
})),
examples,
runs: job.runs.map((r) => ({
id: r.id,
number: r.number,
status: r.status,
created: r.createdAt,
payload: r.event.payload ? JSON.stringify(r.event.payload, null, 2) : undefined,
})),
hasTestRuns: job._count.runs > 0,
};
}
}
@@ -17,16 +17,34 @@ export class TriggerDetailsPresenter {
select: {
event: {
select: {
id: true,
eventId: true,
name: true,
payload: true,
context: true,
timestamp: true,
deliveredAt: true,
externalAccount: {
select: {
identifier: true,
},
},
},
},
},
});
return event;
return {
id: event.eventId,
name: event.name,
payload: JSON.stringify(event.payload, null, 2),
context: JSON.stringify(event.context, null, 2),
timestamp: event.timestamp,
deliveredAt: event.deliveredAt,
externalAccount: event.externalAccount
? {
identifier: event.externalAccount.identifier,
}
: undefined,
};
}
}
@@ -1,17 +1,178 @@
import { ComingSoon } from "~/components/ComingSoon";
import { PageContainer, PageBody } from "~/components/layout/AppLayout";
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import {
ForwardIcon,
SquaresPlusIcon,
UsersIcon,
WrenchScrewdriverIcon,
} from "@heroicons/react/24/solid";
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Header2 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { TextLink } from "~/components/primitives/TextLink";
import { useOrganization } from "~/hooks/useOrganizations";
import { OrganizationParamsSchema, jobPath, organizationTeamPath } from "~/utils/pathBuilder";
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
import { Link } from "@remix-run/react/dist/components";
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server";
import { requireUserId } from "~/services/session.server";
export async function loader({ params, request }: LoaderArgs) {
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const presenter = new OrgUsagePresenter();
const data = await presenter.call({ userId, slug: organizationSlug });
if (!data) {
throw new Response(null, { status: 404 });
}
return typedjson(data);
}
const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
if (active && payload) {
return (
<div className="flex items-center gap-2 rounded border border-border bg-slate-900 px-4 py-2 text-sm text-dimmed">
<p className="text-white">{label}:</p>
<p className="text-white">{payload[0].value}</p>
</div>
);
}
return null;
};
export default function Page() {
const organization = useOrganization();
const loaderData = useTypedLoaderData<typeof loader>();
return (
<PageContainer>
<OrgAdminHeader />
<PageBody>
<ComingSoon
title="Usage & billing"
description="View your usage, tier and billing information. During the beta we will display usage and start billing if you exceed your limits. But don't worry, we'll give you plenty of warning."
icon="billing"
/>
<div className="mb-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Runs this month</Header2>
<ForwardIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.runsCount.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.runsCountLastMonth} runs last month
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Jobs</Header2>
<WrenchScrewdriverIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalJobs.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.totalJobs === loaderData.totalJobsLastMonth ? (
<>No change since last month</>
) : loaderData.totalJobs > loaderData.totalJobsLastMonth ? (
<>+{loaderData.totalJobs - loaderData.totalJobsLastMonth} since last month</>
) : (
<>-{loaderData.totalJobsLastMonth - loaderData.totalJobs} since last month</>
)}
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Integrations</Header2>
<SquaresPlusIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalIntegrations.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.totalIntegrations === loaderData.totalIntegrationsLastMonth ? (
<>No change since last month</>
) : loaderData.totalIntegrations > loaderData.totalIntegrationsLastMonth ? (
<>
+{loaderData.totalIntegrations - loaderData.totalIntegrationsLastMonth} since
last month
</>
) : (
<>
-{loaderData.totalIntegrationsLastMonth - loaderData.totalIntegrations} since
last month
</>
)}
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Team members</Header2>
<UsersIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalMembers.toLocaleString()}</p>
<TextLink
to={organizationTeamPath(organization)}
className="group text-sm text-dimmed hover:text-bright"
>
Manage
<ArrowRightIcon className="-mb-0.5 ml-0.5 h-4 w-4 text-dimmed transition group-hover:translate-x-1 group-hover:text-bright" />
</TextLink>
</div>
</div>
</div>
<div className="flex max-h-[500px] gap-x-4">
<div className="w-1/2 rounded border border-border py-6 pr-2">
<Header2 className="mb-8 pl-6">Job Runs per month</Header2>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={loaderData.chartData}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
/>
<Tooltip cursor={{ fill: "rgba(255,255,255,0.05)" }} content={<CustomTooltip />} />
<Bar dataKey="total" fill="#DB2777" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
<div className="w-1/2 overflow-y-auto rounded border border-border px-3 py-6">
<div className="mb-2 flex items-baseline justify-between border-b border-border px-3 pb-4">
<Header2 className="">Jobs</Header2>
<Header2 className="">Runs</Header2>
</div>
<div className="space-y-2">
{loaderData.jobs.map((job) => (
<Link
to={jobPath(organization, job.project, job)}
className="flex items-center rounded px-4 py-3 transition hover:bg-slate-850"
key={job.id}
>
<div className="space-y-1">
<p className="text-sm font-medium leading-none">{job.slug}</p>
<p className="text-sm text-muted-foreground">Project: {job.project.name}</p>
</div>
<div className="ml-auto font-medium">{job._count.runs.toLocaleString()}</div>
</Link>
))}
</div>
</div>
</div>
</PageBody>
</PageContainer>
);
@@ -19,6 +19,7 @@ import {
PageTitleRow,
} from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Switch } from "~/components/primitives/Switch";
import { TextLink } from "~/components/primitives/TextLink";
import { useFilterJobs } from "~/hooks/useFilterJobs";
import { useOrganization } from "~/hooks/useOrganizations";
@@ -62,8 +63,11 @@ export default function Page() {
const organization = useOrganization();
const project = useProject();
const { jobs } = useTypedLoaderData<typeof loader>();
const { filterText, setFilterText, filteredItems } = useFilterJobs(jobs);
const hasJobs = jobs.length > 0;
const { filterText, setFilterText, filteredItems, onlyActiveJobs, setOnlyActiveJobs } =
useFilterJobs(jobs);
const totalJobs = jobs.length;
const hasJobs = totalJobs > 0;
const activeJobCount = jobs.filter((j) => j.status === "ACTIVE").length;
return (
<PageContainer className={hasJobs ? "" : "grid-rows-1"}>
@@ -74,7 +78,8 @@ export default function Page() {
</PageTitleRow>
<PageInfoRow>
<PageInfoGroup>
<PageInfoProperty icon={"job"} label={"Active Jobs"} value={jobs.length} />
<PageInfoProperty icon={"job"} label={"All Jobs"} value={totalJobs} />
<PageInfoProperty icon={"job"} label={"Active Jobs"} value={activeJobCount} />
</PageInfoGroup>
</PageInfoRow>
</PageHeader>
@@ -96,7 +101,7 @@ export default function Page() {
</Callout>
)}
<div className="mb-2 flex flex-col">
<div className="flex w-full">
<div className="flex w-full gap-x-2">
<Input
placeholder="Search Jobs"
variant="tertiary"
@@ -104,6 +109,14 @@ export default function Page() {
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
autoFocus
/>
<Switch
variant="small"
label="Active Jobs"
checked={onlyActiveJobs}
onCheckedChange={setOnlyActiveJobs}
className={"shrink-0"}
/>
<HelpTrigger title="Example Jobs and inspiration" />
</div>
@@ -160,7 +173,7 @@ function ExampleJobs() {
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"
className="mb-4 border-b border-slate-800"
/>
<Header2 spacing>How to create a Job</Header2>
<Paragraph variant="small" spacing>
@@ -6,7 +6,7 @@ import { useEventSource } from "remix-utils";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { Button } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { Callout, CalloutVariant } from "~/components/primitives/Callout";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { FormError } from "~/components/primitives/FormError";
@@ -18,8 +18,14 @@ import { Paragraph } from "~/components/primitives/Paragraph";
import { Sheet, SheetBody, SheetContent, SheetHeader } from "~/components/primitives/Sheet";
import { ClientEndpoint } from "~/presenters/EnvironmentsPresenter.server";
import { endpointStreamingPath } from "~/utils/pathBuilder";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { EndpointIndexStatus, RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
import {
EndpointIndexStatusIcon,
EndpointIndexStatusLabel,
endpointIndexStatusTitle,
} from "~/components/environments/EndpointIndexStatus";
import { CodeBlock } from "~/components/code/CodeBlock";
type ConfigureEndpointSheetProps = {
slug: string;
@@ -119,15 +125,29 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
method="post"
action={`/resources/environments/${endpoint.environment.id}/endpoint/${endpoint.id}`}
>
<Callout variant="success" className="justiy-between items-center">
<Paragraph variant="small" className="grow text-green-200">
Endpoint configured. Last refreshed:{" "}
{endpoint.latestIndex ? (
<DateTime date={endpoint.latestIndex.updatedAt} />
) : (
""
)}
</Paragraph>
<Callout
variant="info"
icon={
<EndpointIndexStatusIcon status={endpoint.latestIndex?.status ?? "PENDING"} />
}
className="justiy-between items-center"
>
<div className="flex grow items-center gap-2">
<EndpointIndexStatusLabel
status={endpoint.latestIndex?.status ?? "PENDING"}
/>
<Paragraph variant="small" className="grow">
Last refreshed:{" "}
{endpoint.latestIndex ? (
<>
<DateTime date={endpoint.latestIndex.updatedAt} />
</>
) : (
""
)}
</Paragraph>
</div>
<Button
variant="primary/small"
type="submit"
@@ -138,6 +158,11 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
{refreshingEndpoint ? "Refreshing" : "Refresh now"}
</Button>
</Callout>
{endpoint.latestIndex?.error && (
<FormError className="p-2">
<pre>{endpoint.latestIndex.error.message}</pre>
</FormError>
)}
</refreshEndpointFetcher.Form>
</div>
<div className="max-w-full overflow-hidden">
@@ -155,3 +180,16 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
</Sheet>
);
}
function calloutVariantFromStatus(status: EndpointIndexStatus): CalloutVariant {
switch (status) {
case "PENDING":
return "pending";
case "STARTED":
return "pending";
case "SUCCESS":
return "success";
case "FAILURE":
return "error";
}
}
@@ -3,11 +3,16 @@ import { LoaderArgs } from "@remix-run/server-runtime";
import { useEffect, useMemo, useState } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils";
import {
EndpointIndexStatusIcon,
EndpointIndexStatusLabel,
} from "~/components/environments/EndpointIndexStatus";
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { Badge } from "~/components/primitives/Badge";
import { ButtonContent } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header2, Header3 } from "~/components/primitives/Headers";
@@ -38,7 +43,6 @@ import { ProjectParamSchema, projectEnvironmentsStreamingPath } from "~/utils/pa
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) => {
@@ -85,8 +89,8 @@ export default function Page() {
const client = clients.find((c) => c.slug === selected.client);
if (!client) return undefined;
if (selected.type === "PREVIEW" || selected.type === "STAGING") {
throw new Error("PREVIEW/STAGING is not yet supported");
if (selected.type === "PREVIEW") {
throw new Error("PREVIEW is not yet supported");
}
return {
@@ -180,6 +184,7 @@ export default function Page() {
<TableHeaderCell>Environment</TableHeaderCell>
<TableHeaderCell>Url</TableHeaderCell>
<TableHeaderCell>Last refreshed</TableHeaderCell>
<TableHeaderCell>Last refresh Status</TableHeaderCell>
<TableHeaderCell>Jobs</TableHeaderCell>
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
</TableRow>
@@ -195,6 +200,18 @@ export default function Page() {
})
}
/>
{client.endpoints.STAGING && (
<EndpointRow
endpoint={client.endpoints.STAGING}
type="STAGING"
onClick={() =>
setSelected({
client: client.slug,
type: "STAGING",
})
}
/>
)}
<EndpointRow
endpoint={client.endpoints.PRODUCTION}
type="PRODUCTION"
@@ -218,7 +235,7 @@ export default function Page() {
</>
)}
</div>
{selectedEndpoint && (
{selectedEndpoint && selectedEndpoint.endpoint && (
<ConfigureEndpointSheet
slug={selectedEndpoint.clientSlug}
endpoint={selectedEndpoint.endpoint}
@@ -256,7 +273,7 @@ function EndpointRow({
<EnvironmentLabel environment={{ type }} />
</div>
</TableCell>
<TableCell onClick={onClick} colSpan={4} alignment="right">
<TableCell onClick={onClick} colSpan={5} alignment="right">
<div className="flex items-center justify-end gap-4">
<span className="text-amber-500">
The {environmentTitle({ type })} environment is not configured
@@ -278,7 +295,17 @@ function EndpointRow({
<TableCell onClick={onClick}>
{endpoint.latestIndex ? <DateTime date={endpoint.latestIndex.updatedAt} /> : ""}
</TableCell>
<TableCell onClick={onClick}>{endpoint.latestIndex?.stats.jobs ?? ""}</TableCell>
<TableCell onClick={onClick}>
{endpoint.latestIndex ? (
<div className="flex items-center gap-1">
<EndpointIndexStatusIcon status={endpoint.latestIndex.status} />
<EndpointIndexStatusLabel status={endpoint.latestIndex.status} />
</div>
) : (
""
)}
</TableCell>
<TableCell onClick={onClick}>{endpoint.latestIndex?.stats?.jobs ?? ""}</TableCell>
<TableCellChevron onClick={onClick} />
</TableRow>
);
@@ -13,6 +13,7 @@ import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { DateTime } from "~/components/primitives/DateTime";
import { DetailCell } from "~/components/primitives/DetailCell";
import { Header2 } from "~/components/primitives/Headers";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Input } from "~/components/primitives/Input";
@@ -151,7 +152,7 @@ function PossibleIntegrationsList({
onCheckedChange={setOnlyShowIntegrations}
variant="small"
label={
<span className="inline-flex items-center gap-1">
<span className="mt-0.5 inline-flex items-center gap-1">
<IntegrationIcon /> Trigger.dev Integrations
</span>
}
@@ -209,10 +210,12 @@ function PossibleIntegrationsList({
<Feedback
button={
<button className="w-full">
<ExternalIntegrationLink
name="plus"
<DetailCell
leadingIcon="plus"
leadingIconClassName="text-dimmed"
label="Request an API and we'll add it to the list as an Integration"
trailingIcon="chevron-right"
trailingIconClassName="text-slate-700 group-hover:text-bright"
/>
</button>
}
@@ -221,10 +224,12 @@ function PossibleIntegrationsList({
<Header2 className="mb-2 mt-6">Create an Integration</Header2>
<a href="https://docs.trigger.dev/integrations/create" target="_blank">
<ExternalIntegrationLink
name="integration"
<DetailCell
leadingIcon="integration"
leadingIconClassName="text-dimmed"
label="Learn how to create your own API Integrations"
trailingIcon="external-link"
trailingIconClassName="text-slate-700 group-hover:text-bright"
/>
</a>
</div>
@@ -482,77 +487,16 @@ 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-900">
<NamedIconInBox
name={icon ?? identifier}
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
/>
<Paragraph
variant="small"
className="m-0 flex-1 text-left leading-[1.1rem] transition group-hover:text-bright"
>
{name}
</Paragraph>
<div className="flex flex-none items-center gap-1">
{isIntegration && <IntegrationIcon />}
<NamedIcon
name="plus"
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
/>
</div>
</div>
);
}
function ExternalIntegrationLink({
name,
label,
trailingIcon,
}: {
name: string;
label: string;
trailingIcon: string;
}) {
return (
<span className="group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-850">
<NamedIconInBox
name={name}
className="h-9 w-9 flex-none text-dimmed transition group-hover:border-slate-750"
iconClassName="text-dimmed"
/>
<Paragraph variant="base" className="m-0 flex-1 text-left transition group-hover:text-bright">
{label}
</Paragraph>
<div className="flex flex-none items-center gap-1">
<NamedIcon
name={trailingIcon}
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
/>
</div>
</span>
<DetailCell
className="w-full"
leadingIcon={icon ?? identifier}
label={name}
trailingIcon="plus"
trailingIconClassName="text-slate-700 group-hover:text-bright"
/>
);
}
export function IntegrationIcon() {
return <LogoIcon className="h-3.5 w-3.5 flex-none pb-0.5" />;
}
function InfoLink({ text }: { text: string }) {
return (
<div className="group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-850">
<NamedIconInBox
name="integration"
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
/>
<Paragraph variant="base" className="m-0 flex-1 text-left transition group-hover:text-bright">
{text}
</Paragraph>
<div className="flex flex-none items-center gap-1">
<NamedIcon
name="docs"
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
/>
</div>
</div>
);
}
@@ -8,6 +8,7 @@ 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 { Paragraph } from "~/components/primitives/Paragraph";
import { useFilterJobs } from "~/hooks/useFilterJobs";
import { useIntegrationClient } from "~/hooks/useIntegrationClient";
import { JobListPresenter } from "~/presenters/JobListPresenter.server";
@@ -52,10 +53,8 @@ export default function Page() {
{(open) => (
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div className="grow">
<div className="mb-2 flex items-center justify-between gap-x-2">
{jobs.length === 0 ? (
<Header2>Jobs using this integration will appear here</Header2>
) : (
<div className="mb-2 flex items-center justify-end gap-x-2">
{jobs.length !== 0 && (
<Input
placeholder="Search Jobs"
variant="tertiary"
@@ -68,9 +67,9 @@ export default function Page() {
<HelpTrigger title="How do I use this integration?" />
</div>
{jobs.length === 0 ? (
<>
<JobSkeleton />
</>
<div className="mt-8 rounded border border-border px-2 py-6 text-center">
<Paragraph variant="small">Jobs using this Integration will appear here.</Paragraph>
</div>
) : (
<JobsTable
jobs={filteredItems}
@@ -1,6 +1,7 @@
import { useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { PopoverTrigger } from "@radix-ui/react-popover";
import { ClipboardIcon } from "@heroicons/react/20/solid";
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
import { Form, useActionData, useSubmit } from "@remix-run/react";
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
import { useCallback, useRef, useState } from "react";
@@ -8,13 +9,16 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { JSONEditor } from "~/components/code/JSONEditor";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { HowToRunATest } from "~/components/helpContent/HelpContentText";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { DateTime } from "~/components/primitives/DateTime";
import { DetailCell } from "~/components/primitives/DetailCell";
import { FormError } from "~/components/primitives/FormError";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Popover, PopoverContent } from "~/components/primitives/Popover";
import { Header2 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import {
Select,
SelectContent,
@@ -23,12 +27,15 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { TextLink } from "~/components/primitives/TextLink";
import { runStatusClassNameColor, runStatusTitle } from "~/components/runs/RunStatuses";
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { TestJobPresenter } from "~/presenters/TestJobPresenter.server";
import { TestJobService } from "~/services/jobs/testJob.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { isValidIcon } from "~/utils/icon";
import { JobParamsSchema, jobRunDashboardPath, trimTrailingSlash } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -36,14 +43,14 @@ export const loader = async ({ request, params }: LoaderArgs) => {
const { organizationSlug, projectParam, jobParam } = JobParamsSchema.parse(params);
const presenter = new TestJobPresenter();
const { environments, hasTestRuns } = await presenter.call({
const { environments, runs, examples } = await presenter.call({
userId,
organizationSlug,
projectSlug: projectParam,
jobSlug: jobParam,
});
return typedjson({ environments, hasTestRuns });
return typedjson({ environments, runs, examples });
};
const schema = z.object({
@@ -69,6 +76,7 @@ const schema = z.object({
}),
environmentId: z.string(),
versionId: z.string(),
accountId: z.string().optional(),
});
//todo save the chosen environment to a cookie (for that user), use it to default the env dropdown
@@ -84,11 +92,7 @@ export const action: ActionFunction = async ({ request, params }) => {
}
const testService = new TestJobService();
const run = await testService.call({
environmentId: submission.value.environmentId,
payload: submission.value.payload,
versionId: submission.value.versionId,
});
const run = await testService.call(submission.value);
if (!run) {
return redirectBackWithErrorMessage(
@@ -116,21 +120,30 @@ export const handle: Handle = {
const startingJson = "{\n\n}";
export default function Page() {
const { environments, runs, examples } = useTypedLoaderData<typeof loader>();
//form submission
const submit = useSubmit();
const lastSubmission = useActionData();
const [isExamplePopoverOpen, setIsExamplePopoverOpen] = useState(false);
const { environments, hasTestRuns } = useTypedLoaderData<typeof loader>();
const [defaultJson, setDefaultJson] = useState<string>(startingJson);
const currentJson = useRef<string>(defaultJson);
//examples
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(
examples.at(0)?.id ?? runs.at(0)?.id
);
const selectedCodeSample =
examples.find((e) => e.id === selectedCodeSampleId)?.payload ??
runs.find((r) => r.id === selectedCodeSampleId)?.payload;
const [defaultJson, setDefaultJson] = useState<string>(selectedCodeSample ?? startingJson);
const setCode = useCallback((code: string) => {
setDefaultJson(code);
}, []);
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string>(environments[0].id);
const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId);
const insertCode = useCallback((code: string) => {
setDefaultJson(code);
setIsExamplePopoverOpen(false);
}, []);
const currentJson = useRef<string>(defaultJson);
const [currentAccountId, setCurrentAccountId] = useState<string | undefined>(undefined);
const submitForm = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
@@ -139,6 +152,7 @@ export default function Page() {
payload: currentJson.current,
environmentId: selectedEnvironmentId,
versionId: selectedEnvironment?.versionId ?? "",
...(currentAccountId ? { accountId: currentAccountId } : {}),
},
{
action: "",
@@ -147,10 +161,10 @@ export default function Page() {
);
e.preventDefault();
},
[currentJson, selectedEnvironmentId]
[currentJson, selectedEnvironmentId, currentAccountId]
);
const [form, { environmentId, payload }] = useForm({
const [form, { environmentId, payload, accountId }] = useForm({
id: "test-job",
lastSubmission,
onValidate({ formData }) {
@@ -168,103 +182,178 @@ export default function Page() {
}
return (
<Help defaultOpen={true}>
{(open) => (
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div className="flex h-fit max-h-full overflow-hidden">
<Form
className="flex max-h-full grow flex-col gap-2 overflow-y-auto"
method="post"
{...form.props}
onSubmit={(e) => submitForm(e)}
>
<div className="flex flex-none items-center justify-between gap-2">
<div className="flex flex-none items-center gap-2">
<SelectGroup>
<Select
name="environment"
value={selectedEnvironmentId}
onValueChange={setSelectedEnvironmentId}
>
<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>
<div className="grid h-full grid-cols-1 gap-4">
<div className="flex h-full max-h-full overflow-hidden">
<Form
className="flex h-full max-h-full grow flex-col gap-4 overflow-y-auto"
method="post"
{...form.props}
onSubmit={(e) => submitForm(e)}
>
<div className="grid h-full grid-cols-[1fr_auto] overflow-hidden">
<div className="relative h-full flex-1 overflow-hidden rounded-l border border-border">
<JSONEditor
defaultValue={defaultJson}
readOnly={false}
basicSetup
onChange={(v) => {
currentJson.current = v;
{selectedEnvironment && selectedEnvironment.examples.length > 0 && (
<Popover
open={isExamplePopoverOpen}
onOpenChange={(open) => setIsExamplePopoverOpen(open)}
//deselect the example if it's been edited
if (selectedCodeSampleId) {
if (v !== selectedCodeSample) {
setDefaultJson(v);
setSelectedCodeSampleId(undefined);
}
}
}}
height="100%"
min-height="100%"
max-height="100%"
autoFocus
placeholder="Use your schema to enter valid JSON or add one of the example payloads then click 'Run test'"
className="h-full"
/>
</div>
<div className="flex h-full w-fit min-w-[20rem] flex-col gap-4 overflow-y-auto rounded-r border border-l-0 border-border p-4">
{examples.length > 0 && (
<div className="flex flex-col gap-2">
<Header2>Example payloads</Header2>
{examples.map((example) => (
<button
type="button"
key={example.id}
onClick={(e) => {
setCode(example.payload ?? "");
setSelectedCodeSampleId(example.id);
}}
>
<PopoverTrigger>
<ButtonContent
variant="secondary/small"
LeadingIcon="beaker"
TrailingIcon="chevron-down"
>
Insert an example
</ButtonContent>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="start">
{selectedEnvironment?.examples.map((example) => (
<Button
key={example.id}
variant="menu-item"
onClick={(e) => insertCode(example.payload)}
LeadingIcon={example.icon ?? "beaker"}
fullWidth
textAlignLeft
>
{example.name}
</Button>
))}
</PopoverContent>
</Popover>
)}
<DetailCell
leadingIcon={isValidIcon(example.icon) ? example.icon : CodeBracketIcon}
leadingIconClassName="text-blue-500"
label={example.name}
trailingIcon={example.id === selectedCodeSampleId ? "check" : "plus"}
trailingIconClassName={
example.id === selectedCodeSampleId
? "text-green-500 group-hover:text-green-400"
: "text-slate-500 group-hover:text-bright"
}
/>
</button>
))}
</div>
<HelpTrigger title="How do I run a test?" />
</div>
<div className="flex-1 overflow-auto rounded border border-slate-850 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<JSONEditor
defaultValue={defaultJson}
readOnly={false}
basicSetup
onChange={(v) => (currentJson.current = v)}
minHeight="150px"
/>
</div>
<div className="flex flex-none items-center justify-between">
{payload.error ? (
<FormError id={payload.errorId}>{payload.error}</FormError>
)}
<div className="flex flex-col gap-2">
<Header2>Recent payloads</Header2>
{runs.length === 0 ? (
<Callout variant="info">
Recent payloads will show here once you've completed a Run.
</Callout>
) : (
<div />
<div className="flex flex-col gap-2">
{runs.map((run) => (
<button
key={run.id}
type="button"
onClick={(e) => {
setCode(run.payload ?? "");
setSelectedCodeSampleId(run.id);
}}
>
<DetailCell
leadingIcon={ClockIcon}
leadingIconClassName="text-slate-400"
label={<DateTime date={run.created} />}
description={
<>
Run #{run.number}{" "}
<span className={runStatusClassNameColor(run.status)}>
{runStatusTitle(run.status).toLocaleLowerCase()}
</span>
</>
}
trailingIcon={run.id === selectedCodeSampleId ? "check" : "plus"}
trailingIconClassName={
run.id === selectedCodeSampleId
? "text-green-500 group-hover:text-green-400"
: "text-slate-500 group-hover:text-bright"
}
/>
</button>
))}
</div>
)}
<Button
type="submit"
variant="primary/medium"
LeadingIcon="beaker"
leadingIconClassName="text-bright"
>
Run test
</Button>
</div>
</Form>
{selectedEnvironment?.hasAuthResolver && (
<div className="flex flex-col gap-2">
<Header2>Account ID</Header2>
<InputGroup fullWidth>
<Input
type="text"
fullWidth
variant="large"
value={currentAccountId}
placeholder={`e.g. abc_1234`}
onChange={(e) => setCurrentAccountId(e.target.value)}
/>
<FormError>{accountId.error}</FormError>
<Hint>
Learn about testing Jobs with an Account ID in our{" "}
<TextLink href="https://trigger.dev/docs/documentation/guides/using-integrations-byo-auth#testing-jobs-with-account-id">
BYOAuth docs
</TextLink>
</Hint>
</InputGroup>
</div>
)}
</div>
</div>
<HelpContent title="How to run a test" className="h-fit">
<HowToRunATest />
</HelpContent>
</div>
)}
</Help>
<div className="flex items-center justify-between">
<LinkButton
variant="tertiary/medium"
to="https://trigger.dev/docs/documentation/guides/testing-jobs"
TrailingIcon="external-link"
>
Learn more about running tests
</LinkButton>
<div className="flex flex-none items-center justify-end gap-2">
{payload.error ? (
<FormError id={payload.errorId}>{payload.error}</FormError>
) : (
<div />
)}
<SelectGroup>
<Select
name="environment"
value={selectedEnvironmentId}
onValueChange={setSelectedEnvironmentId}
>
<SelectTrigger size="medium">
<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>
<Button
type="submit"
variant="primary/medium"
LeadingIcon="beaker"
leadingIconClassName="text-bright"
shortcut={{ key: "enter", modifiers: ["mod"], enabledOnInputElements: true }}
>
Run test
</Button>
</div>
</div>
</Form>
</div>
</div>
);
}
@@ -1,21 +1,112 @@
import { AstroLogo } from "~/assets/logos/AstroLogo";
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { Feedback } from "~/components/Feedback";
import { PageGradient } from "~/components/PageGradient";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
import { StepContentContainer } from "~/components/StepContentContainer";
import { InlineCode } from "~/components/code/InlineCode";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import {
ClientTabs,
ClientTabsContent,
ClientTabsList,
ClientTabsTrigger,
} from "~/components/primitives/ClientTabs";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { Header1 } from "~/components/primitives/Headers";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { Handle } from "~/utils/handle";
import { trimTrailingSlash } from "~/utils/pathBuilder";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
import { Callout } from "~/components/primitives/Callout";
import { Badge } from "~/components/primitives/Badge";
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Astro" />,
};
export default function Page() {
export default function SetUpAstro() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const appOrigin = useAppOrigin();
return (
<FrameworkComingSoon
frameworkName="Astro"
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/452"
githubIssueNumber={452}
>
<AstroLogo className="w-56" />
</FrameworkComingSoon>
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-12 grid place-items-center">
<AstroLogo className="w-64" />
</div>
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 5 minutes
</Header1>
<div className="flex items-center gap-2">
<LinkButton
to={projectSetupPath(organization, project)}
variant="tertiary/small"
LeadingIcon={Squares2X2Icon}
>
Choose a different framework
</LinkButton>
<Feedback
button={
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
</div>
<div>
<Callout
variant={"info"}
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
className="mb-8"
>
Trigger.dev has full support for serverless. We will be adding support for long-running
servers soon.
</Callout>
<div>
<StepNumber
stepNumber="1"
title="Run the CLI 'init' command in an existing Astro 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">example.ts</InlineCode> to help you
get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Astro app" />
<StepContentContainer>
<RunDevCommand />
</StepContentContainer>
<StepNumber stepNumber="3" 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>
</div>
</div>
</div>
</PageGradient>
);
}
@@ -1,21 +1,120 @@
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
import { Feedback } from "~/components/Feedback";
import { PageGradient } from "~/components/PageGradient";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
import { StepContentContainer } from "~/components/StepContentContainer";
import { InlineCode } from "~/components/code/InlineCode";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { Handle } from "~/utils/handle";
import { trimTrailingSlash } from "~/utils/pathBuilder";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Express" />,
};
export default function Page() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const appOrigin = useAppOrigin();
return (
<FrameworkComingSoon
frameworkName="Express"
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/451"
githubIssueNumber={451}
>
<ExpressLogo className="w-56" />
</FrameworkComingSoon>
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-12 grid place-items-center">
<ExpressLogo className="w-64" />
</div>
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 5 minutes
</Header1>
<div className="flex items-center gap-2">
<LinkButton
to={projectSetupPath(organization, project)}
variant="tertiary/small"
LeadingIcon={Squares2X2Icon}
>
Choose a different framework
</LinkButton>
<Feedback
button={
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
</div>
<div>
<Callout
variant={"info"}
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
className="mb-8"
>
Trigger.dev has full support for serverless. We will be adding support for long-running
servers soon.
</Callout>
<div>
<StepNumber
stepNumber="1"
title="Manually set up Trigger.dev in your existing Express project"
/>
<StepContentContainer className="flex flex-col gap-2">
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
<div className="mb-2 flex w-full items-center justify-between">
<ClipboardField
secure
className="w-fit"
value={devEnvironment.apiKey}
variant={"secondary/medium"}
icon={<Badge variant="outline">Server</Badge>}
/>
</div>
<Paragraph>Now follow this guide:</Paragraph>
<LinkButton
to="https://trigger.dev/docs/documentation/guides/manual/express"
variant="primary/medium"
TrailingIcon="external-link"
>
Manual installation guide
</LinkButton>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Express app" />
<StepContentContainer>
<RunDevCommand />
<Callout variant="info">
You may be using the `start` script instead, in which case substitute `dev` in the
above commands.
</Callout>
</StepContentContainer>
<StepNumber stepNumber="3" 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>
</div>
</div>
</div>
</PageGradient>
);
}
@@ -1,21 +1,220 @@
import { NestjsLogo } from "~/assets/logos/NestjsLogo";
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { Feedback } from "~/components/Feedback";
import { PageGradient } from "~/components/PageGradient";
import { StepContentContainer } from "~/components/StepContentContainer";
import { InlineCode } from "~/components/code/InlineCode";
import { InstallPackages } from "~/components/code/InstallPackages";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { Handle } from "~/utils/handle";
import { trimTrailingSlash } from "~/utils/pathBuilder";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
import { CodeBlock } from "../../components/code/CodeBlock";
import { TriggerDevStep } from "~/components/SetupCommands";
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Nest.js" />,
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="NestJS" />,
};
export default function Page() {
const AppModuleCode = `
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TriggerDevModule } from '@trigger.dev/nestjs';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TriggerDevModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
id: 'my-nest-app',
apiKey: config.getOrThrow('TRIGGER_API_KEY'),
apiUrl: config.getOrThrow('TRIGGER_API_URL'),
verbose: false,
ioLogLocalEnabled: true,
}),
}),
],
})
export class AppModule {}
`;
const JobControllerCode = `
import { Controller, Get } from '@nestjs/common';
import { InjectTriggerDevClient } from '@trigger.dev/nestjs';
import { eventTrigger, TriggerClient } from '@trigger.dev/sdk';
@Controller()
export class JobController {
constructor(
@InjectTriggerDevClient() private readonly client: TriggerClient,
) {
this.client.defineJob({
id: 'test-job',
name: 'Test Job One',
version: '0.0.1',
trigger: eventTrigger({
name: 'test.event',
}),
run: async (payload, io, ctx) => {
await io.logger.info('Hello world!', { payload });
return {
message: 'Hello world!',
};
},
});
}
@Get()
getHello(): string {
return \`Running Trigger.dev with client-id \${this.client.id}\`;
}
}`;
const AppModuleWithControllerCode = `
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TriggerDevModule } from '@trigger.dev/nestjs';
import { JobController } from './job.controller';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TriggerDevModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
id: 'my-nest-app',
apiKey: config.getOrThrow('TRIGGER_API_KEY'),
apiUrl: config.getOrThrow('TRIGGER_API_URL'),
verbose: false,
ioLogLocalEnabled: true,
}),
}),
],
controllers: [
//...existingControllers,
JobController
],
})
export class AppModule {}
`;
const packageJsonCode = `"trigger.dev": {
"endpointId": "my-nest-app"
}`;
export default function SetupNestJS() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
const appOrigin = useAppOrigin();
invariant(devEnvironment, "devEnvironment is required");
return (
<FrameworkComingSoon
frameworkName="Nest.js"
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/449"
githubIssueNumber={449}
>
<NestjsLogo className="w-56" />
</FrameworkComingSoon>
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 2 minutes
</Header1>
<div className="flex items-center gap-2">
<LinkButton
to={projectSetupPath(organization, project)}
variant="tertiary/small"
LeadingIcon={Squares2X2Icon}
>
Choose a different framework
</LinkButton>
<Feedback
button={
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
</div>
<>
<StepNumber stepNumber="1" title="Add the dependencies" />
<StepContentContainer>
<InstallPackages
packages={["@trigger.dev/sdk", "@trigger.dev/nestjs", "@nestjs/config"]}
/>
</StepContentContainer>
<StepNumber stepNumber="2" title="Add the environment variables" />
<StepContentContainer className="flex flex-col gap-2">
<Paragraph>
Inside your <InlineCode>.env</InlineCode> file, create the following env variables:
</Paragraph>
<CodeBlock
fileName=".env"
showChrome
code={`TRIGGER_API_KEY=${devEnvironment.apiKey}\nTRIGGER_API_URL=${appOrigin}`}
/>
</StepContentContainer>
<StepNumber stepNumber="3" title="Add the TriggerDevModule" />
<StepContentContainer className="flex flex-col gap-2">
<Paragraph>
Now, go to your <InlineCode>app.module.ts</InlineCode> and add the{" "}
<InlineCode>TriggerDevModule</InlineCode>:
</Paragraph>
<CodeBlock fileName="app.module.ts" showChrome code={AppModuleCode} />
</StepContentContainer>
<StepNumber stepNumber="4" title="Add the first job" />
<StepContentContainer className="flex flex-col gap-2">
<Paragraph>
Create a <InlineCode>controller</InlineCode> called{" "}
<InlineCode>job.controller.ts</InlineCode> and add the following code:
</Paragraph>
<CodeBlock fileName="src/job.controller.ts" showChrome code={JobControllerCode} />
</StepContentContainer>
<StepNumber stepNumber="5" title="Update your app.module.ts" />
<StepContentContainer className="flex flex-col gap-2">
<Paragraph>
Now, add the new <InlineCode>controller</InlineCode> to your{" "}
<InlineCode>app.module.ts</InlineCode>:
</Paragraph>
<CodeBlock fileName="app.module.ts" showChrome code={AppModuleWithControllerCode} />
</StepContentContainer>
<StepNumber stepNumber="6" title="Update your package.json" />
<StepContentContainer className="flex flex-col gap-2">
<Paragraph>
Now, add this to the top-level of your <InlineCode>package.json</InlineCode>:
</Paragraph>
<CodeBlock fileName="package.json" showChrome code={packageJsonCode} />
</StepContentContainer>
<StepNumber stepNumber="7" title="Run your app" />
<StepContentContainer className="flex flex-col gap-2">
<Paragraph>
Finally, run your project with <InlineCode>npm run start</InlineCode>:
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="8" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="9" title="Wait for Jobs" displaySpinner />
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</>
</div>
</PageGradient>
);
}
@@ -28,6 +28,7 @@ import { useProject } from "~/hooks/useProject";
import { Handle } from "~/utils/handle";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
import { Callout } from "~/components/primitives/Callout";
import { NextjsLogo } from "~/assets/logos/NextjsLogo";
type SelectionChoices = "use-existing-project" | "create-new-next-app";
@@ -48,6 +49,9 @@ export default function SetupNextjs() {
return (
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-12 grid place-items-center">
<NextjsLogo className="w-56" />
</div>
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
@@ -25,8 +25,9 @@ import { useProject } from "~/hooks/useProject";
import { Handle } from "~/utils/handle";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
import { Callout } from "~/components/primitives/Callout";
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
import { Badge } from "~/components/primitives/Badge";
import { RemixLogo } from "~/assets/logos/RemixLogo";
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Remix" />,
@@ -38,9 +39,14 @@ export default function SetUpRemix() {
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const appOrigin = useAppOrigin();
return (
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-12 grid place-items-center">
<RemixLogo className="w-64" />
</div>
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 5 minutes
@@ -75,28 +81,16 @@ export default function SetUpRemix() {
<div>
<StepNumber
stepNumber="1"
title="Follow the steps from the Remix manual installation guide"
title="Run the CLI 'init' command in an existing Remix project"
/>
<StepContentContainer className="flex flex-col gap-2">
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
<div className="mb-2 flex w-full items-center justify-between">
<ClipboardField
secure
className="w-fit"
value={devEnvironment.apiKey}
variant={"secondary/medium"}
icon={<Badge variant="outline">Server</Badge>}
/>
</div>
<Paragraph>Now follow this guide:</Paragraph>
<LinkButton
to="https://trigger.dev/docs/documentation/guides/manual/remix"
variant="primary/medium"
TrailingIcon="external-link"
>
Manual installation guide
</LinkButton>
<div className="flex items-start justify-start gap-2"></div>
<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">example.server.ts</InlineCode> to
help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Remix app" />
<StepContentContainer>
@@ -1,23 +1,113 @@
import { SvelteKitLogo } from "~/assets/logos/SveltekitLogo";
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { Feedback } from "~/components/Feedback";
import { PageGradient } from "~/components/PageGradient";
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
import { StepContentContainer } from "~/components/StepContentContainer";
import { InlineCode } from "~/components/code/InlineCode";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { Header1 } from "~/components/primitives/Headers";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { Handle } from "~/utils/handle";
import { trimTrailingSlash } from "~/utils/pathBuilder";
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
import { Callout } from "~/components/primitives/Callout";
import { Badge } from "~/components/primitives/Badge";
export const handle: Handle = {
breadcrumb: (match) => (
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="SvelteKit" />
),
};
export default function Page() {
export default function SetUpSveltekit() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
return (
<FrameworkComingSoon
frameworkName="SvelteKit"
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/453"
githubIssueNumber={453}
>
<SvelteKitLogo className="w-56" />
</FrameworkComingSoon>
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in 5 minutes
</Header1>
<div className="flex items-center gap-2">
<LinkButton
to={projectSetupPath(organization, project)}
variant="tertiary/small"
LeadingIcon={Squares2X2Icon}
>
Choose a different framework
</LinkButton>
<Feedback
button={
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
</div>
<div>
<Callout
variant={"info"}
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
className="mb-8"
>
Trigger.dev has full support for serverless. We will be adding support for long-running
servers soon.
</Callout>
<div>
<StepNumber
stepNumber="1"
title="Follow the steps from the Sveltekit manual installation guide"
/>
<StepContentContainer className="flex flex-col gap-2">
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
<div className="mb-2 flex w-full items-center justify-between">
<ClipboardField
secure
className="w-fit"
value={devEnvironment.apiKey}
variant={"secondary/medium"}
icon={<Badge variant="outline">Server</Badge>}
/>
</div>
<Paragraph>Now follow this guide:</Paragraph>
<LinkButton
to="https://trigger.dev/docs/documentation/guides/manual/sveltekit"
variant="primary/medium"
TrailingIcon="external-link"
>
Manual installation guide
</LinkButton>
<div className="flex items-start justify-start gap-2"></div>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your sveltekit app" />
<StepContentContainer>
<RunDevCommand extra=" -- --open --host" />
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStep extra=" --port 5173" />
</StepContentContainer>
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</div>
</div>
</div>
</PageGradient>
);
}
@@ -81,13 +81,19 @@ class CreateExternalConnectionService {
environment: AuthenticatedEnvironment,
payload: CreateExternalConnectionBody
) {
const externalAccount = await this.#prismaClient.externalAccount.findUniqueOrThrow({
const externalAccount = await this.#prismaClient.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: accountIdentifier,
},
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: accountIdentifier,
},
update: {},
});
const integration = await this.#prismaClient.integration.findUniqueOrThrow({
@@ -0,0 +1,68 @@
import { ActionArgs, json } from "@remix-run/server-runtime";
import {
EndpointIndexErrorSchema,
GetEndpointIndexResponse,
GetEndpointIndexResponseSchema,
} from "@trigger.dev/core";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server";
import { logger } from "~/services/logger.server";
const ParamsSchema = z.object({
indexId: z.string(),
});
export async function loader({ request, params }: ActionArgs) {
if (request.method.toUpperCase() !== "GET") {
return { status: 405, body: "Method Not Allowed" };
}
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
logger.info("Invalid or missing api key", { url: request.url });
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const authenticatedEnv = authenticationResult.environment;
const { indexId } = parsedParams.data;
const endpointIndex = await prisma.endpointIndex.findUnique({
where: {
id: indexId,
endpoint: {
environmentId: authenticatedEnv.id,
},
},
});
if (!endpointIndex) {
logger.info("EndpointIndex not found", { url: request.url });
return json({ error: "EndpointIndex not found" }, { status: 404 });
}
const parsed = GetEndpointIndexResponseSchema.safeParse(endpointIndex);
if (!parsed.success) {
logger.info("EndpointIndex failed parsing", { errors: parsed.error.issues, endpointIndex });
const parseFailResult: GetEndpointIndexResponse = {
status: "FAILURE",
error: {
message: "Invalid endpoint index",
},
updatedAt: new Date(),
};
return json(parseFailResult, { status: 500 });
}
return json(parsed.data);
}
@@ -1,7 +1,7 @@
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 { $transaction, PrismaClient, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { workerQueue } from "~/services/worker.server";
import { safeJsonParse } from "~/utils/json";
@@ -93,43 +93,53 @@ export class TriggerEndpointIndexHookService {
body,
});
const endpoint = await this.#prismaClient.endpoint.findUnique({
where: {
environmentId_slug: {
environmentId,
slug: endpointSlug,
await $transaction(this.#prismaClient, async (tx) => {
const endpoint = await tx.endpoint.findUnique({
where: {
environmentId_slug: {
environmentId,
slug: endpointSlug,
},
},
},
include: {
environment: true,
},
});
include: {
environment: true,
},
});
if (!endpoint) {
throw new Error("Endpoint not found");
}
if (endpoint.indexingHookIdentifier !== indexHookIdentifier) {
throw new Error("Index hook identifier is invalid");
}
const reason = parseReasonFromBody(body);
// Index the endpoint in 5 seconds from now
await workerQueue.enqueue(
"indexEndpoint",
{
id: endpoint.id,
source: "HOOK",
reason,
sourceData: body,
},
{
runAt: new Date(Date.now() + 5000),
maxAttempts:
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
if (!endpoint) {
throw new Error("Endpoint not found");
}
);
if (endpoint.indexingHookIdentifier !== indexHookIdentifier) {
throw new Error("Index hook identifier is invalid");
}
const reason = parseReasonFromBody(body);
const index = await tx.endpointIndex.create({
data: {
endpointId: endpoint.id,
status: "PENDING",
source: "HOOK",
reason,
sourceData: body,
},
});
// Index the endpoint in 5 seconds from now
await workerQueue.enqueue(
"performEndpointIndexing",
{
id: index.id,
},
{
runAt: new Date(Date.now() + 5000),
maxAttempts:
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
tx,
}
);
});
}
}
@@ -0,0 +1,71 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { PrismaErrorSchema } from "~/db.server";
import { z } from "zod";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CancelRunService } from "~/services/runs/cancelRun.server";
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
const ParamsSchema = z.object({
runId: 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" };
}
// Authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
}
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return json({ error: "Invalid or Missing runId" }, { status: 400 });
}
const { runId } = parsed.data;
const service = new CancelRunService();
try {
await service.call({ runId });
} catch (error) {
const prismaError = PrismaErrorSchema.safeParse(error);
// Record not found in the database
if (prismaError.success && prismaError.data.code === "P2005") {
return json({ error: "Run not found" }, { status: 404 });
} else {
return json({ error: "Internal Server Error" }, { status: 500 });
}
}
const presenter = new ApiRunPresenter();
const jobRun = await presenter.call({
runId: runId,
});
if (!jobRun) {
return json({ message: "Run not found" }, { status: 404 });
}
return json({
id: jobRun.id,
status: jobRun.status,
startedAt: jobRun.startedAt,
updatedAt: jobRun.updatedAt,
completedAt: jobRun.completedAt,
output: jobRun.output,
tasks: jobRun.tasks,
statuses: jobRun.statuses.map((s) => ({
...s,
state: s.state ?? undefined,
data: s.data ?? undefined,
history: s.history ?? undefined,
})),
});
}
@@ -0,0 +1,151 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { TaskStatus } from "@trigger.dev/database";
import {
RunTaskBodyOutput,
RunTaskBodyOutputSchema,
ServerTask,
StatusHistory,
StatusHistorySchema,
StatusUpdate,
StatusUpdateData,
StatusUpdateSchema,
StatusUpdateState,
} from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ulid } from "~/services/ulid.server";
import { workerQueue } from "~/services/worker.server";
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
const ParamsSchema = z.object({
runId: z.string(),
id: z.string(),
});
export async function action({ request, params }: ActionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "PUT") {
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 { runId, id } = ParamsSchema.parse(params);
// Now parse the request body
const anyBody = await request.json();
logger.debug("SetStatusService.call() request body", {
body: anyBody,
runId,
id,
});
const body = StatusUpdateSchema.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new SetStatusService();
try {
const statusRecord = await service.call(runId, id, body.data);
logger.debug("SetStatusService.call() response body", {
runId,
id,
statusRecord,
});
if (!statusRecord) {
return json({ error: "Something went wrong" }, { status: 500 });
}
const status = JobRunStatusRecordSchema.parse({
...statusRecord,
state: statusRecord.state ?? undefined,
history: statusRecord.history ?? undefined,
data: statusRecord.data ?? undefined,
});
return json(status);
} catch (error) {
if (error instanceof Error) {
return json({ error: error.message }, { status: 400 });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
export class SetStatusService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(runId: string, id: string, status: StatusUpdate) {
const statusRecord = await $transaction(this.#prismaClient, async (tx) => {
const existingStatus = await tx.jobRunStatusRecord.findUnique({
where: {
runId_key: {
runId,
key: id,
},
},
});
const history: StatusHistory = [];
const historyResult = StatusHistorySchema.safeParse(existingStatus?.history);
if (historyResult.success) {
history.push(...historyResult.data);
}
if (existingStatus) {
history.push({
label: existingStatus.label,
state: (existingStatus.state ?? undefined) as StatusUpdateState,
data: (existingStatus.data ?? undefined) as StatusUpdateData,
});
}
const updatedStatus = await tx.jobRunStatusRecord.upsert({
where: {
runId_key: {
runId,
key: id,
},
},
create: {
key: id,
runId,
//this shouldn't ever use the id in reality, as the SDK makess it compulsory on the first call
label: status.label ?? id,
state: status.state,
data: status.data as any,
history: [],
},
update: {
label: status.label,
state: status.state,
data: status.data as any,
history: history as any[],
},
});
return updatedStatus;
});
return statusRecord;
}
}
@@ -0,0 +1,82 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { apiCors } from "~/utils/apiCors";
const ParamsSchema = z.object({
runId: z.string(),
});
const RecordsSchema = z.array(JobRunStatusRecordSchema);
export async function loader({ request, params }: LoaderArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request, { allowPublicKey: true });
if (!authenticationResult) {
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
}
const { runId } = ParamsSchema.parse(params);
logger.debug("Get run statuses", {
runId,
});
try {
const run = await prisma.jobRun.findUnique({
where: {
id: runId,
},
select: {
id: true,
status: true,
output: true,
statuses: {
orderBy: {
createdAt: "asc",
},
},
},
});
if (!run) {
return apiCors(request, json({ error: `No run found for id ${runId}` }, { status: 404 }));
}
const parsedStatuses = RecordsSchema.parse(
run.statuses.map((s) => ({
...s,
state: s.state ?? undefined,
data: s.data ?? undefined,
history: s.history ?? undefined,
}))
);
return apiCors(
request,
json({
run: {
id: run.id,
status: run.status,
output: run.output,
},
statuses: parsedStatuses,
})
);
} catch (error) {
if (error instanceof Error) {
return apiCors(request, json({ error: error.message }, { status: 400 }));
}
return apiCors(request, json({ error: "Something went wrong" }, { status: 500 }));
}
}
@@ -0,0 +1,124 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { z } from "zod";
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
import { logger } from "~/services/logger.server";
const ParamsSchema = z.object({
runId: z.string(),
id: z.string(),
secret: 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" };
}
const { runId, id } = ParamsSchema.parse(params);
// Parse body as JSON (no schema parsing)
const body = await request.json();
const service = new CallbackRunTaskService();
try {
// Complete task with request body as output
await service.call(runId, id, body, request.url);
return json({ success: true });
} catch (error) {
if (error instanceof Error) {
logger.error("Error while processing task callback:", { error });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
export class CallbackRunTaskService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(runId: string, id: string, taskBody: any, callbackUrl: string): Promise<void> {
const task = await findTask(prisma, id);
if (!task) {
return;
}
if (task.runId !== runId) {
return;
}
if (task.status !== "WAITING") {
return;
}
if (!task.callbackUrl) {
return;
}
if (new URL(task.callbackUrl).pathname !== new URL(callbackUrl).pathname) {
logger.error("Callback URLs don't match", { runId, taskId: id, callbackUrl });
return;
}
logger.debug("CallbackRunTaskService.call()", { task });
await this.#resumeTask(task, taskBody);
}
async #resumeTask(task: NonNullable<FoundTask>, output: any) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.taskAttempt.updateMany({
where: {
taskId: task.id,
status: "PENDING",
},
data: {
status: "COMPLETED",
},
});
await tx.task.update({
where: { id: task.id },
data: {
status: "COMPLETED",
completedAt: new Date(),
output: output ? output : undefined,
},
});
await this.#resumeRunExecution(task, tx);
});
}
async #resumeRunExecution(task: NonNullable<FoundTask>, prisma: PrismaClientOrTransaction) {
await enqueueRunExecutionV2(task.run, prisma, {
skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
});
}
}
type FoundTask = Awaited<ReturnType<typeof findTask>>;
async function findTask(prisma: PrismaClientOrTransaction, id: string) {
return prisma.task.findUnique({
where: { id },
include: {
run: {
include: {
environment: true,
queue: true,
},
},
},
});
}
@@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime";
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
import { CompleteTaskBodyInputSchema } from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -86,8 +86,8 @@ export class CompleteRunTaskService {
): Promise<ServerTask | undefined> {
// Using a transaction, we'll first check to see if the task already exists and return if if it does
// If it doesn't exist, we'll create it and return it
const task = await this.#prismaClient.$transaction(async (prisma) => {
const existingTask = await prisma.task.findUnique({
const task = await this.#prismaClient.$transaction(async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
id,
},
@@ -129,35 +129,31 @@ export class CompleteRunTaskService {
return existingTask;
}
const task = await $transaction(prisma, async (tx) => {
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
},
});
}
return await tx.task.update({
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id,
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
output: taskBody.output ?? undefined,
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
},
});
});
}
return task;
return await tx.task.update({
where: {
id,
},
data: {
status: "COMPLETED",
output: taskBody.output ?? undefined,
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
},
});
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
@@ -2,7 +2,7 @@ import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { FailTaskBodyInput, FailTaskBodyInputSchema, ServerTask } from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -86,8 +86,8 @@ export class FailRunTaskService {
): Promise<ServerTask | undefined> {
// Using a transaction, we'll first check to see if the task already exists and return if if it does
// If it doesn't exist, we'll create it and return it
const task = await this.#prismaClient.$transaction(async (prisma) => {
const existingTask = await prisma.task.findUnique({
const task = await this.#prismaClient.$transaction(async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
id,
},
@@ -129,35 +129,31 @@ export class FailRunTaskService {
return existingTask;
}
const task = await $transaction(prisma, async (tx) => {
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "ERRORED",
error: formatError(taskBody.error),
},
});
}
return await prisma.task.update({
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id,
id: existingTask.attempts[0].id,
},
data: {
status: "ERRORED",
output: taskBody.error ?? undefined,
completedAt: new Date(),
},
include: {
attempts: true,
error: formatError(taskBody.error),
},
});
});
}
return task;
return await tx.task.update({
where: {
id,
},
data: {
status: "ERRORED",
output: taskBody.error ?? undefined,
completedAt: new Date(),
},
include: {
attempts: true,
},
});
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
@@ -1,14 +1,22 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { TaskStatus } from "@trigger.dev/database";
import { RunTaskBodyOutput, RunTaskBodyOutputSchema, ServerTask } from "@trigger.dev/core";
import {
API_VERSIONS,
RunTaskBodyOutput,
RunTaskBodyOutputSchema,
RunTaskResponseWithCachedTasksBody,
ServerTask,
} from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import { prepareTasksForCaching, taskWithAttemptsToServerTask } from "~/models/task.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ulid } from "~/services/ulid.server";
import { workerQueue } from "~/services/worker.server";
import { generateSecret } from "~/services/sources/utils.server";
import { env } from "~/env.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -16,6 +24,8 @@ const ParamsSchema = z.object({
const HeadersSchema = z.object({
"idempotency-key": z.string(),
"trigger-version": z.string().optional().nullable(),
"x-cached-tasks-cursor": z.string().optional().nullable(),
});
export async function action({ request, params }: ActionArgs) {
@@ -37,7 +47,11 @@ export async function action({ request, params }: ActionArgs) {
return json({ error: "Invalid or Missing idempotency key" }, { status: 400 });
}
const { "idempotency-key": idempotencyKey } = headers.data;
const {
"idempotency-key": idempotencyKey,
"trigger-version": triggerVersion,
"x-cached-tasks-cursor": cachedTasksCursor,
} = headers.data;
const { runId } = ParamsSchema.parse(params);
@@ -48,6 +62,8 @@ export async function action({ request, params }: ActionArgs) {
body: anyBody,
runId,
idempotencyKey,
triggerVersion,
cachedTasksCursor,
});
const body = RunTaskBodyOutputSchema.safeParse(anyBody);
@@ -71,6 +87,26 @@ export async function action({ request, params }: ActionArgs) {
return json({ error: "Something went wrong" }, { status: 500 });
}
if (triggerVersion === API_VERSIONS.LAZY_LOADED_CACHED_TASKS) {
const requestMigration = new ChangeRequestLazyLoadedCachedTasks();
const responseBody = await requestMigration.call(runId, task, cachedTasksCursor);
logger.debug(
"RunTaskService.call() response migrating with ChangeRequestLazyLoadedCachedTasks",
{
responseBody,
cachedTasksCursor,
}
);
return json(responseBody, {
headers: {
"trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
},
});
}
return json(task);
} catch (error) {
if (error instanceof Error) {
@@ -81,6 +117,51 @@ export async function action({ request, params }: ActionArgs) {
}
}
class ChangeRequestLazyLoadedCachedTasks {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
runId: string,
task: ServerTask,
cursor?: string | null
): Promise<RunTaskResponseWithCachedTasksBody> {
if (!cursor) {
return {
task,
};
}
// We need to limit the cached tasks to not be too large >2MB when serialized
const TOTAL_CACHED_TASK_BYTE_LIMIT = 2000000;
const nextTasks = await this.#prismaClient.task.findMany({
where: {
runId,
status: "COMPLETED",
noop: false,
},
take: 250,
cursor: {
id: cursor,
},
orderBy: {
id: "asc",
},
});
const preparedTasks = prepareTasksForCaching(nextTasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
return {
task,
cachedTasks: preparedTasks,
};
}
}
export class RunTaskService {
#prismaClient: PrismaClient;
@@ -106,10 +187,13 @@ export class RunTaskService {
},
});
const delayUntilInFuture = taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now();
const callbackEnabled = taskBody.callback?.enabled;
if (existingTask) {
if (existingTask.status === "CANCELED") {
const existingTaskStatus =
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
delayUntilInFuture || callbackEnabled || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
@@ -154,16 +238,21 @@ export class RunTaskService {
status = "CANCELED";
} else {
status =
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
delayUntilInFuture || callbackEnabled || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
}
const taskId = ulid();
const callbackUrl = callbackEnabled
? `${env.APP_ORIGIN}/api/v1/runs/${runId}/tasks/${taskId}/callback/${generateSecret(12)}`
: undefined;
const task = await tx.task.create({
data: {
id: ulid(),
id: taskId,
idempotencyKey,
displayKey: taskBody.displayKey,
runConnection: taskBody.connectionKey
@@ -191,9 +280,10 @@ export class RunTaskService {
noop: taskBody.noop,
delayUntil: taskBody.delayUntil,
params: taskBody.params ?? undefined,
properties: taskBody.properties ?? undefined,
properties: this.#filterProperties(taskBody.properties) ?? undefined,
redact: taskBody.redact ?? undefined,
operation: taskBody.operation,
callbackUrl,
style: taskBody.style ?? { style: "normal" },
attempts: {
create: {
@@ -215,8 +305,19 @@ export class RunTaskService {
{
id: task.id,
},
{ tx, runAt: task.delayUntil ?? undefined }
{ tx, runAt: task.delayUntil ?? undefined, jobKey: `operation:${task.id}` }
);
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
if (taskBody.callback.timeoutInSeconds > 0) {
// We need to schedule the callback timeout
await workerQueue.enqueue(
"processCallbackTimeout",
{
id: task.id,
},
{ tx, runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000) }
);
}
}
return task;
@@ -224,4 +325,14 @@ export class RunTaskService {
return task ? taskWithAttemptsToServerTask(task) : undefined;
}
#filterProperties(properties: RunTaskBodyOutput["properties"]): RunTaskBodyOutput["properties"] {
if (!properties) return;
return properties.filter((property) => {
if (!property) return false;
return typeof property.label === "string" && typeof property.text === "string";
});
}
}
+15 -43
View File
@@ -1,9 +1,8 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { cors } from "remix-utils";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest, getApiKeyFromRequest } from "~/services/apiAuth.server";
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { apiCors } from "~/utils/apiCors";
import { taskListToTree } from "~/utils/taskListToTree";
@@ -52,48 +51,15 @@ export async function loader({ request, params }: LoaderArgs) {
const query = parsedQuery.data;
const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE";
const take = Math.min(query.take, 50);
const jobRun = await prisma.jobRun.findUnique({
where: {
id: runId,
},
select: {
id: true,
status: true,
startedAt: true,
updatedAt: true,
completedAt: true,
environmentId: true,
output: true,
tasks: {
select: {
id: true,
parentId: true,
displayKey: true,
status: true,
name: true,
icon: true,
startedAt: true,
completedAt: true,
params: showTaskDetails,
output: showTaskDetails,
},
where: {
parentId: query.subtasks ? undefined : null,
},
orderBy: {
id: "asc",
},
take: take + 1,
cursor: query.cursor
? {
id: query.cursor,
}
: undefined,
},
},
const presenter = new ApiRunPresenter();
const jobRun = await presenter.call({
runId: runId,
maxTasks: take,
taskDetails: showTaskDetails,
subTasks: query.subtasks,
cursor: query.cursor,
});
if (!jobRun) {
@@ -122,6 +88,12 @@ export async function loader({ request, params }: LoaderArgs) {
const { parentId, ...rest } = task;
return { ...rest };
}),
statuses: jobRun.statuses.map((s) => ({
...s,
state: s.state ?? undefined,
data: s.data ?? undefined,
history: s.history ?? undefined,
})),
nextCursor: nextTask ? nextTask.id : undefined,
})
);
@@ -6,11 +6,24 @@ import { HandleHttpSourceService } from "~/services/sources/handleHttpSource.ser
export async function action({ request, params }: ActionArgs) {
logger.info("Handling http source", { url: request.url });
const { id } = z.object({ id: z.string() }).parse(params);
try {
const { id } = z.object({ id: z.string() }).parse(params);
const service = new HandleHttpSourceService();
const result = await service.call(id, request);
const service = new HandleHttpSourceService();
return await service.call(id, request);
return new Response(undefined, {
status: result.status,
});
} catch (e) {
if (e instanceof Error) {
logger.error("Error handling http source", { error: e.message });
} else {
logger.error("Error handling http source", { error: JSON.stringify(e) });
}
return new Response(undefined, {
status: 500,
});
}
}
export async function loader({ request, params }: LoaderArgs) {
+16 -3
View File
@@ -60,16 +60,29 @@ export default function LoginPage() {
<a href="https://trigger.dev">
<LogoIcon className="mb-4 h-16 w-16" />
</a>
<FormTitle divide={false} title="Log in to Trigger.dev" />
<FormTitle divide={false} title="Welcome to Trigger.dev" className="mb-2 pb-0" />
<Paragraph variant="small" className="mb-4">
Create an account or login
</Paragraph>
<Fieldset>
<div className="flex flex-col gap-y-2">
{data.showGithubAuth && (
<Button type="submit" variant="primary/large" fullWidth>
<Button
type="submit"
variant="primary/large"
fullWidth
data-action="continue with github"
>
<NamedIcon name={"github"} className={"mr-1.5 h-4 w-4"} />
Continue with GitHub
</Button>
)}
<LinkButton to="/login/magic" variant="secondary/large" fullWidth>
<LinkButton
to="/login/magic"
variant="secondary/large"
fullWidth
data-action="continue with email"
>
<NamedIcon
name={"envelope"}
className={"mr-1.5 h-4 w-4 text-dimmed transition group-hover:text-bright"}
+56 -13
View File
@@ -1,6 +1,6 @@
import type { ActionArgs, LoaderArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form, useTransition } from "@remix-run/react";
import { Form, useNavigation, useTransition } from "@remix-run/react";
import { TypedMetaFunction, typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { LogoIcon } from "~/components/LogoIcon";
@@ -17,10 +17,10 @@ import { Paragraph } from "~/components/primitives/Paragraph";
import { authenticator } from "~/services/auth.server";
import { commitSession, getUserSession } from "~/services/sessionStorage.server";
import magicLinkIcon from "./login.magic.svg";
import type { LoaderType as RootLoader } from "~/root";
import { appEnvTitleTag } from "~/utils";
import { TextLink } from "~/components/primitives/TextLink";
import { FormError } from "~/components/primitives/FormError";
export const meta: TypedMetaFunction<typeof loader, { root: RootLoader }> = ({ parentsData }) => ({
title: `Login to Trigger.dev${appEnvTitleTag(parentsData?.root.appEnv)}`,
@@ -32,10 +32,26 @@ export async function loader({ request }: LoaderArgs) {
});
const session = await getUserSession(request);
const error = session.get("auth:error");
return typedjson({
magicLinkSent: session.has("triggerdotdev:magiclink"),
});
let magicLinkError: string | undefined;
if (error) {
if ("message" in error) {
magicLinkError = error.message;
} else {
magicLinkError = JSON.stringify(error, null, 2);
}
}
return typedjson(
{
magicLinkSent: session.has("triggerdotdev:magiclink"),
magicLinkError,
},
{
headers: { "Set-Cookie": await commitSession(session) },
}
);
}
export async function action({ request }: ActionArgs) {
@@ -50,7 +66,7 @@ export async function action({ request }: ActionArgs) {
.parse(payload);
if (action === "send") {
await authenticator.authenticate("email-link", request, {
return authenticator.authenticate("email-link", request, {
successRedirect: "/login/magic",
failureRedirect: "/login/magic",
});
@@ -67,13 +83,13 @@ export async function action({ request }: ActionArgs) {
}
export default function LoginMagicLinkPage() {
const { magicLinkSent } = useTypedLoaderData<typeof loader>();
const transition = useTransition();
const { magicLinkSent, magicLinkError } = useTypedLoaderData<typeof loader>();
const navigate = useNavigation();
const isLoading =
(transition.state === "loading" || transition.state === "submitting") &&
transition.type === "actionSubmission" &&
transition.submission.formData.get("action") === "send";
(navigate.state === "loading" || navigate.state === "submitting") &&
navigate.formAction !== undefined &&
navigate.formData?.get("action") === "send";
return (
<AppContainer showBackgroundGradient={true}>
@@ -102,12 +118,17 @@ export default function LoginMagicLinkPage() {
variant="tertiary/small"
LeadingIcon="arrow-left"
leadingIconClassName="text-dimmed group-hover:text-bright transition"
data-action="re-enter email"
>
Re-enter email
</Button>
}
confirmButton={
<LinkButton to="/login" variant="tertiary/small">
<LinkButton
to="/login"
variant="tertiary/small"
data-action="log in using another option"
>
Log in using another option
</LinkButton>
}
@@ -116,7 +137,10 @@ export default function LoginMagicLinkPage() {
</>
) : (
<>
<FormTitle divide={false} title="Log in to Trigger.dev" />
<FormTitle divide={false} title="Welcome to Trigger.dev" className="mb-2 pb-0" />
<Paragraph variant="small" className="mb-4 text-center">
Create an account or login using your email
</Paragraph>
<Fieldset className="flex w-full flex-col items-center gap-y-2">
<InputGroup>
<Label>Your email address</Label>
@@ -137,6 +161,7 @@ export default function LoginMagicLinkPage() {
variant="primary/medium"
disabled={isLoading}
fullWidth
data-action="send a magic link"
>
<NamedIcon
name={isLoading ? "spinner-white" : "envelope"}
@@ -144,6 +169,7 @@ export default function LoginMagicLinkPage() {
/>
{isLoading ? "Sending…" : "Send a magic link"}
</Button>
{magicLinkError && <FormError>{magicLinkError}</FormError>}
</Fieldset>
<Paragraph variant="extra-small" className="my-4 text-center">
By logging in with your email you agree to our{" "}
@@ -162,11 +188,28 @@ export default function LoginMagicLinkPage() {
variant={"tertiary/small"}
LeadingIcon={"arrow-left"}
leadingIconClassName="text-dimmed group-hover:text-bright transition"
data-action="all login options"
>
All login options
</LinkButton>
</>
)}
<div className="mt-8 rounded border border-border px-6 py-4">
<Paragraph variant="small" className="mb-2 text-center">
Having login issues?
</Paragraph>
<Paragraph variant="extra-small" className="text-center">
Ensure the Magic Link email isn't in your spam folder. If the problem persists,{" "}
<TextLink href="mailto:help@trigger.dev" target="_blank">
drop us an email
</TextLink>{" "}
or let us know on{" "}
<TextLink href="https://trigger.dev/discord" target="_blank">
Discord
</TextLink>
.
</Paragraph>
</div>
</div>
</Form>
</MainCenteredContainer>
@@ -1,11 +1,5 @@
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 { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server";
import { requireUserId } from "~/services/session.server";
@@ -2,28 +2,15 @@ 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 { CreateEndpointError } from "~/services/endpoints/createEndpoint.server";
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);
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData, { schema: bodySchema });
@@ -48,7 +35,7 @@ export async function action({ request, params }: ActionArgs) {
}
const service = new ValidateCreateEndpointService();
const result = await service.call({
await service.call({
url: submission.value.url,
environment,
});
+18 -6
View File
@@ -1,4 +1,4 @@
import type { DeliverEmail } from "emails";
import type { DeliverEmail, SendPlainTextOptions } from "emails";
import { EmailClient } from "emails";
import type { SendEmailOptions } from "remix-auth-email-link";
import { redirect } from "remix-typedjson";
@@ -6,6 +6,7 @@ import { env } from "~/env.server";
import type { User } from "~/models/user.server";
import type { AuthUser } from "./authUser";
import { workerQueue } from "./worker.server";
import { logger } from "./logger.server";
const client = new EmailClient({
apikey: env.RESEND_API_KEY,
@@ -20,11 +21,22 @@ export async function sendMagicLinkEmail(options: SendEmailOptions<AuthUser>): P
throw redirect(options.magicLink);
}
return client.send({
email: "magic_link",
to: options.emailAddress,
magicLink: options.magicLink,
});
logger.debug("Sending magic link email", { emailAddress: options.emailAddress });
try {
return await client.send({
email: "magic_link",
to: options.emailAddress,
magicLink: options.magicLink,
});
} catch (error) {
logger.error("Error sending magic link email", { error: JSON.stringify(error) });
throw error;
}
}
export async function sendPlainTextEmail(options: SendPlainTextOptions) {
return client.sendPlainText(options);
}
export async function scheduleWelcomeEmail(user: User) {
@@ -5,6 +5,7 @@ import { findOrCreateUser } from "~/models/user.server";
import { env } from "~/env.server";
import { sendMagicLinkEmail } from "~/services/email.server";
import { postAuthentication } from "./postAuth.server";
import { logger } from "./logger.server";
let secret = env.MAGIC_LINK_SECRET;
if (!secret) throw new Error("Missing MAGIC_LINK_SECRET env variable.");
@@ -25,6 +26,8 @@ const emailStrategy = new EmailLinkStrategy(
form: FormData;
magicLinkVerify: boolean;
}) => {
logger.info("Magic link user authenticated", { email, magicLinkVerify });
try {
const { user, isNewUser } = await findOrCreateUser({
email,
@@ -35,6 +38,7 @@ const emailStrategy = new EmailLinkStrategy(
return { userId: user.id };
} catch (error) {
logger.debug("Magic link user failed to authenticate", { error: JSON.stringify(error) });
throw error;
}
}
+28 -59
View File
@@ -1,7 +1,9 @@
import {
API_VERSIONS,
ApiEventLog,
DeliverEventResponseSchema,
DeserializedJson,
EndpointHeadersSchema,
ErrorWithStackSchema,
HttpSourceRequest,
HttpSourceResponseSchema,
@@ -89,10 +91,20 @@ export class EndpointApi {
};
}
const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries()));
if (headers.success && headers.data["trigger-version"]) {
return {
...pongResponse.data,
triggerVersion: headers.data["trigger-version"],
};
}
return pongResponse.data;
}
async indexEndpoint() {
const startTimeInMs = performance.now();
const response = await safeFetch(this.url, {
method: "POST",
headers: {
@@ -102,66 +114,13 @@ export class EndpointApi {
},
});
if (!response) {
throw new 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) {
throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`);
}
const anyBody = await response.json();
const data = IndexEndpointResponseSchema.parse(anyBody);
return {
ok: true,
data,
} as const;
}
async deliverEvent(event: ApiEventLog) {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-trigger-api-key": this.apiKey,
"x-trigger-action": "DELIVER_EVENT",
},
body: JSON.stringify(event),
});
if (!response) {
throw new Error(`Could not connect to endpoint ${this.url}`);
}
if (!response.ok) {
throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`);
}
const anyBody = await response.json();
logger.debug("deliverEvent() response from endpoint", {
body: anyBody,
});
return DeliverEventResponseSchema.parse(anyBody);
response,
headerParser: EndpointHeadersSchema,
parser: IndexEndpointResponseSchema,
errorParser: ErrorWithStackSchema,
durationInMs: Math.floor(performance.now() - startTimeInMs),
};
}
async executeJobRequest(options: RunJobBody) {
@@ -338,6 +297,15 @@ export class EndpointApi {
};
}
const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries()));
if (headers.success && headers.data["trigger-version"]) {
return {
...validateResponse.data,
triggerVersion: headers.data["trigger-version"],
};
}
return validateResponse.data;
}
}
@@ -359,6 +327,7 @@ function addStandardRequestOptions(options: RequestInit) {
headers: {
...options.headers,
"user-agent": "triggerdotdev-server/2.0.0",
"x-trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
},
};
}

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