Compare commits

...

375 Commits

Author SHA1 Message Date
Matt Aitken 7358fcb891 Latest lockfile 2024-01-30 17:07:24 +00:00
Matt Aitken 28052daad9 Faster jobs page (#879)
* Optional next/previous with the list pagination

* Defer the loading of the runs table

* Only return the latest run in a separate query, then do a join in code

* Added a composite index to speed up getting the latest run from a job id
2024-01-30 17:02:04 +00:00
github-actions[bot] 364c8c5f7f chore: Update version for release (#876)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-30 13:36:36 +00:00
Matt Aitken 4b3b418abb Set endpoint URLs to null, instead of deleting them (#878)
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Added Endpoint deletedAt column

* Only show endpoints where they’re not deleted

* Don’t delete Endpoints, set the deletedAt and change their slug name

* Only perform indexing if the endpoint isn’t deleted

* Have a nullable URL for endpoints

* Deal with null URLs throughout the app

* Re-running and retrying behaves properly when there’s no endpoint URL

* Remove console.log

* Better error message when doing a run
2024-01-30 10:45:12 +00:00
Matt Aitken 2e354d342c Deleting endpoint working (#875) 2024-01-29 11:57:25 +00:00
Matt Aitken dd879c8e4a Fix api run statuses (#874)
* Added a subtask for testing

* Make it easier to run the CLI from the nextjs reference project

* Copies of the run and statuses endpoints, but without simplifying the run statuses

* Use the new v2 endpoints that give the full run statuses

* Removed unused import

* v2 events endpoint with the full run status info

* Changeset
2024-01-29 11:42:19 +00:00
Matt Aitken af485b9180 Fix custom oauth client (#872)
* A checkbox can now be readonly and not be checkable, with correct styling

* Use readOnly for the hasCustomClient checkbox, not disabled

* Better styling of the disabled state

* The update oauth form too
2024-01-26 17:25:15 +00:00
Matt Aitken a739ebaa88 Tweaked the layout of the usage runs charts so the daily runs are in the box with other run charts 2024-01-26 15:05:52 +00:00
Kritik Jiyaviya 3ebc2578e0 feat: Add daily runs graph (#865)
* feat: Add daily runs graph

* Merge branch 'main' into feat/daily-runs-graph

* update DayRunsChart

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-26 15:00:30 +00:00
Kritik Jiyaviya 0b657b33f9 Allow retrying a job run with an invalid payload (#869) 2024-01-26 12:39:23 +00:00
Erin Allison 7ac942dc0e Address unintended type coercion in PageInfoPropertyComponent (#861)
Fixes triggerdotdev/trigger.dev#858

Signed-off-by: Erin Allison <erin@eallison.us>
2024-01-26 09:44:55 +00:00
Matt Aitken 9b12016428 Improved SQL reads for some dashboard pages (#868)
* Pass subscription status into the usage bar

* Page navigation spinner is now blue (was a bit subtle before)

* Better logging of db queries, this will be commented out before the PR is merged

* Select only the required fields

* We don’t need the member count for each org

* WIP redirecting with projectId in session

* Switching projects is now working, without duplicating the project query

* Removed logs in revalidate function

* Removes some unused imports

* ProjectPresenter: removed lots of unused db selects

* Root use defaultShouldRevalidate, not just true

* Simplified the job list query and separated the deleting job modal query

* Use requireUserId instead of requireUser wherever possible

* EventListPresenter query simplified

* Simplified the RunListPresenter query

* Disable query logging

* Use the latest updated version for the job list table

* We need to use the org presenter on the select plan page
2024-01-26 09:41:11 +00:00
Matt Aitken d6b44de4ba Defer the loading of the usage data (#866)
🚀 Publish Trigger.dev Docker / units (push) Failing after 10s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 20s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 19s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Updated to the latest remix-typedjson package

* Throw an error if the org isn’t found

* Use defer. Doesn’t work because Date isn’t deserialized correctly

* Use regular defer, typeddefer isn’t working properly…

* Don’t update remix-typedjson, wasn’t needed

* Organize imports
2024-01-22 10:26:33 +00:00
Matt Aitken 17df4839d7 Latest lockfile
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 5s
🚀 Publish Trigger.dev Docker / units (push) Failing after 21s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2024-01-21 17:30:21 +00:00
github-actions[bot] e3f78178f7 chore: Update version for release (#864)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-21 17:29:23 +00:00
Kritik Jiyaviya da90ee13c3 fix: Resend integration example comment (#860) 2024-01-21 17:16:26 +00:00
Matt Aitken 583da458ec Changed the minimum interval time period from 60s to 20s (with changeset) 2024-01-21 17:06:09 +00:00
Matt Aitken dcf95c4eb2 Revert "Changeset: Changed the minimum interval time from 60s to 20s"
This reverts commit c272e38de2.
2024-01-21 17:04:11 +00:00
Matt Aitken 32cf5790cb Revert "Changed the minimum interval time from 60s to 20s"
This reverts commit ca78ddc2c2.
2024-01-21 17:04:11 +00:00
Matt Aitken c272e38de2 Changeset: Changed the minimum interval time from 60s to 20s 2024-01-21 16:29:54 +00:00
Matt Aitken ca78ddc2c2 Changed the minimum interval time from 60s to 20s 2024-01-21 16:28:28 +00:00
Matt Aitken 07ed8c346a Fix for enabled env var in docs 2024-01-20 14:01:56 +00:00
Matt Aitken d4391f2e2d Fixed link in io.logger to the run log 2024-01-20 12:54:11 +00:00
Eric Allam 2c328ba93d Fix pnmp lock file again 2024-01-17 14:50:04 +00:00
github-actions[bot] 68dbc07ee8 chore: Update version for release (#854)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-17 14:49:19 +00:00
Eric Allam 58448243af Fix pnpm i 2024-01-17 14:41:51 +00:00
Eric Allam 6c4047cf21 Fix an issue where runs were stuck executing when a child task failed and the parent task retried 2024-01-17 14:39:06 +00:00
github-actions[bot] 831860eace chore: Update version for release (#853)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-17 14:13:34 +00:00
Matt Aitken db46f2a69a Add explicit return type to deprecated sendEmail function because tsup was complaining 2024-01-17 14:07:45 +00:00
Kritik Jiyaviya 4dd6cf18dc feat: Add support for the new Resend audience API endpoints (#850)
* feat: Add support for the new Resend audience API endpoints

* update pnpm-lock.yaml

* update pnpm-lock.yaml

* Merge branch 'main' into feat/support-for-resend-audience-api

* latest pnpm-lock.yaml

* Create lovely-items-end.md

* Update pnpm-lock.yaml

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-17 14:06:45 +00:00
Eric Allam adf66d23b8 Update pnpm lock file
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 4s
🚀 Publish Trigger.dev Docker / units (push) Failing after 15s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2024-01-17 11:15:30 +00:00
github-actions[bot] 5c42831a60 chore: Update version for release (#846)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-17 11:14:52 +00:00
Eric Allam 26f5e7774d Fix yalt.dev connection timeouts by increasing timeout from 1s to 5s (#851)
* Increase the yalt tunnel connection timeout to 5 seconds

* Add changeset
2024-01-17 10:27:57 +00:00
Matt Aitken f209a3b364 Ability to disable organization runs + more obvious if you're over the free limit (#848)
* Fix for typo in error when a Job invoke fails

* Can disable runs for an Organization

* Don’t perform runs if runsEnabled === false for an Org

* Exceeded runs messages made into errors, they were very subtle

* Return the runsEnabled property for each org

* More obvious message if you’re over the free run limit

* Added a red border to the free usage panel if you’re over your run limit

* Added missing property to Storybook for the FreePlanUsage component
2024-01-17 09:42:58 +00:00
Son Yen Nguyen a93b554f8b Feat: Update cron parser human readable string (#847)
* Update cron parser human readable string

* Create hot-cobras-flow.md

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-16 17:22:35 +00:00
Matt Aitken 0f342cd1be Don't show duplicate Job warning if it's an internal job 2024-01-16 15:54:20 +00:00
Matt Aitken 7df2f85a1f Latest lockfile 2024-01-16 15:22:21 +00:00
Matt Aitken f14180d13c Also hide "dev.trigger.scheduled" events from the event list 2024-01-16 15:21:45 +00:00
github-actions[bot] ab6b9514cd chore: Update version for release (#844)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-16 15:00:28 +00:00
Matt Aitken 98345d67d9 Allow unchecked array access in the CLI tsconfig. This matches the other packages… 2024-01-16 14:46:31 +00:00
Matt Aitken 38f5a90399 Auto-yielding is skipped for tasks that are no-ops and subtasks 2024-01-16 14:01:05 +00:00
Matt Aitken 1b2635ae4a Don’t show “trigger.scheduled” events in the list 2024-01-16 12:14:08 +00:00
Matt Aitken 129f023d11 EventTrigger source wasn’t getting passed through to the class 2024-01-16 12:07:11 +00:00
Matt Aitken 795e637dec caniuse-lite updated, it was logging out an annoying message 2024-01-16 11:52:08 +00:00
Matt Aitken 5238c424fc Absolute date filtering (#845)
* WIP on absolute date filtering for runs

* Use the date hook instead

* Reworked the date field again so the state behaves nicely

* Way better date filtering

* Setting the absolute date is working well, also clearing filters

* Reverse date format

* Turn off the guide

* Added time filtering and clearing to the events page
2024-01-16 11:44:15 +00:00
Matt Aitken 1bbd7e6dc3 V3 CLI started, with Personal Access Tokens and Who Am I (#841)
* Initial CLI commit

* Hooked up the CLI so it actually shows stuff…

* Totally reworked the v3 CLI to be based on our existing CLI (Commander)

* WIP on PersonalAccessTokens and AuthorizationCodes

* WIP creating a Personal Access Tokens page. Created a new sidenav for account pages

* Creating tokens is working but the form is broken

* Tokens are created in the UI

* Creating and revoking access tokens from the UI is working

* Improved the create form and copy

* Tokens are a bit shorter and only lowercase

* API endpoint for creating AuthorizationCodes and the web page users hit to create PATs from them

* V3_ENABLED env var and hook that can be used in the UI to show/hide things

* API route to get a PAT (within 10 mins of creating an auth code). Moved some code to core

* Start to build the login command

* Nicer banner when starting the CLI

* Nicer update checking

* Update command style improved

* Removed the template step

* Login command options are now working

* The new CLI is logging in using the Personal Access TOken. But I need to save it still

* Logging in and saving the token is working

* Deal with already being logged in

* Who Am I working with PAT

* Deleted old account side menu header

* Improved the copy on the Auth code page

* Added docs for the login and whoami commands

* Added readme instructions for the update command

* Removed some unused things

* Remove fetchUseProxy for now
2024-01-15 14:45:41 +00:00
Kritik Jiyaviya 3bb82ed9a5 feat: runs filtering by relative timeframe (#832)
* feat: runs filtering by relative timeframe

* improve logic

* Used formatDateTime

* fix: logic of  determineTimeFrame() function

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-15 14:13:20 +00:00
Abhimanyu Yadav ff4ff869ab feat(logger): accepting Error objects as parameters in io.logger.error() (#797)
* feat(logger): accepting Error objects as parameters in io.logger.error()

* Allow errors for any log level

* Create silent-ties-vanish.md

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-15 14:02:41 +00:00
Matt Aitken 1dcee2b338 Changed docs analytics to PostHog EU 2024-01-15 13:51:02 +00:00
Aditya Tripathi 0a798446c8 feat: managed cloud check for search indexing (#825)
* feat: managed cloud check for search indexing

* fix: vice versa on the robots meta tag

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-15 09:35:43 +00:00
Kritik Jiyaviya 209942a63d fix: integrations list scrollbar (#829) 2024-01-15 09:29:42 +00:00
Manthan Mallikarjun 57a9b35870 Fix typo in deployment-automatic docs (#842)
Env var is wrong
2024-01-15 09:28:37 +00:00
Fabian B a16b65f666 docs: fix syntax error (#839) 2024-01-14 16:55:01 +00:00
Matt Aitken 5871745a92 Latest lockfile 2024-01-12 14:13:32 +00:00
github-actions[bot] 87b5dfbf11 chore: Update version for release (#837)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-12 14:10:42 +00:00
Michelle Rosenberger 5af2003516 Fix Stripe integration webhook validation (#836)
The SubtleCryptoProvider cannot be used in a synchronous context. This
is a problem for non-node environments where the Stripe client falls
back to SubtleCryptoProvider.

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-12 14:05:33 +00:00
Matt Aitken 85ce729bf2 Latest lockfile 2024-01-12 13:55:03 +00:00
Matt Aitken 5701d1da42 Reference the —https flag in the instructions if your endpoint isn’t found 2024-01-12 13:54:36 +00:00
Chigala bc61d83764 feat: updates CLI to use https flag (#814)
* feat: updates CLI to use https flag

* added changeset

* Added yalt to the changeset

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-12 13:53:43 +00:00
nicktrn 5bf125be0d Fix: helm chart and docs (#827)
* Improve helm chart and docs

* Minor docs changes

* Add alternative commands to specify values
2024-01-11 13:54:22 +00:00
Eric Allam babe1c0e54 Fix pnpm lock file 2024-01-10 10:58:07 +00:00
github-actions[bot] 1224fceb18 chore: Update version for release (#831)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-10 10:57:32 +00:00
Eric Allam 8277f4d249 Use correct overload param when invoking a job outside of a run (fixes #802) 2024-01-10 10:50:12 +00:00
Eric Allam 73cb8839a5 Fixed invoke inferred payload types (fix #830) 2024-01-10 10:44:35 +00:00
Matt Aitken 5d0a731cf6 Event page tweaks (#826)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 3s
🚀 Publish Trigger.dev Docker / units (push) Failing after 10s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Don’t show internal events

* Show the cancelled time

* Made the Pagination component accept any compatible list type

* Changed some wording on the event detail page
2024-01-05 12:39:27 +00:00
Kritik Jiyaviya 25a152517e feat: Events list page (#824)
* feat: add events list

* Changed the Events icon in the sidemenu

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-05 12:04:31 +00:00
Matt Aitken d1092fcd2c Run filtering now works on the job runs page too (#823)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 3s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 3s
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Run filtering is now shared: runs page and job runs page

* Use optimistic location

* Improved the type in the run status filter dropdown

* Organize imports
2024-01-03 15:50:45 +00:00
Matt Aitken 61d33ed1b8 Latest lockfile 2024-01-03 14:23:21 +00:00
github-actions[bot] 7a51612fc8 chore: Update version for release (#822)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-03 13:48:31 +00:00
Abhimanyu Yadav 4d380173c8 feat:runs filtering by status and environment (#800)
* feat: view teammates' job runs on dev env

* fix typechecks

* feat: add filter functionality on task run page

* feat:adding filter functionality on run task page

* changes filter from csr to ssr

* changing filter functionality from csr to ssr

* adding url.string() for navigation purpose

* improving state management in filtering

* When setting a filter, delete the cursor and direction

* Some design improvements for the "All" options

---------

Co-authored-by: hmacr <hmac.devo@gmail.com>
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-03 13:37:50 +00:00
Hemachandar 8880d8afe2 feat: view teammates' dev env job runs (#708)
* feat: view teammates' job runs on dev env

* fix typechecks

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-03 13:11:22 +00:00
Jan Henning ef037af457 Add a comment explaining that JSON parse middlewares need to come after the Trigger middleware (#773)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 3s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 20s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2024-01-03 11:19:34 +00:00
Dan 390ac6101e Added new http-endpoint examples, changed examples links in the docs (#775)
* updated companyicons package

* Changed custom help so it doesn’t show code examples

* Added Asana / Instagram / Salesforce / Segment / Square / Svix / Snyk / Todoist & Zapier http endpoint examples to the integration page

* Commented out some of the apis we dont have code examples for

* Uncommented raycast and reddit

* Updated the blank state copy

* Changed the copy so it works better on examples with and without code

* Added Brex / Discoed / Google Sheets, Docs, Drive / Gmail / PagerDuty / YouTube endpoint examples

* Added more examples

* Change the top tab to ‘Examples’ and updated /examples links to point to trigger.dev/apis

* Update package.json

* Updated pnpm-lock file

* Updated pnpm-lock file

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-03 11:18:44 +00:00
Dan aa97bf4a52 Changed posthog to eu (#772)
* Updated posthog url and key

* Create clean-plums-happen.md

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-03 10:43:14 +00:00
Matt Aitken 86d6e102a4 Added documentation for cancelRunsForJob 2024-01-03 10:38:23 +00:00
Matt Aitken adf497bc3e Example in the job-catalog of canceling all runs for a Job 2024-01-03 10:31:56 +00:00
Kritik Jiyaviya f7bf25f03f feat: Add ability to cancel all runs for job from SDK (#819)
* feat: Add ability to cancel all runs for job from SDK

* Create nine-trainers-vanish.md

* Cancel runs that are executing and waiting

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-03 10:30:19 +00:00
GuyBorderless f8cab96fa2 use --env cli flag for deno (#820)
* --env cli flag

* Update hono.mdx

* Update hono.mdx
2024-01-03 10:27:39 +00:00
nicktrn 225614fea3 Feature: ARM builds (#821)
* Add native ARM runner

* Switch to arch-agnostic manifest digest

* Set correct latest tag
2024-01-02 15:48:53 +00:00
Yash Khokhaneshiya 740b7b2385 feat: Add $not to eventFilters (#815)
* feat: Add  to eventFilters

* not filter test case added for false evaluation

* Added a patch changeset for @trigger.dev/core

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-01-02 15:43:02 +00:00
Matt Aitken 26d223ab36 Latest lockfile 2023-12-22 15:50:13 +00:00
github-actions[bot] 0c4fd9a631 chore: Update version for release (#813)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-22 15:43:15 +00:00
Matt Aitken 5060eec01b Update Octokit to account for CVE-2023-50728 (#812) 2023-12-22 15:35:47 +00:00
Matt Aitken aaea064208 Added “graphile” to the logs when they zodWorker starts, to make it easier for self hosters to debug 2023-12-22 10:46:05 +00:00
Matt Aitken 9346e4358d Latest lockfile 2023-12-22 10:43:33 +00:00
github-actions[bot] 3aafd0480b chore: Update version for release (#798)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-22 10:40:55 +00:00
Matt Aitken 771feb5395 Awaiting completion with threads/runs now works with the "requires_action" status 2023-12-22 10:13:44 +00:00
Matt Aitken 03703af682 Only append a search param if the value is defined 2023-12-20 11:06:40 +00:00
Eric Allam 5359af1242 Add logpush true to yalt wrangler 2023-12-18 10:58:30 +00:00
Eric Allam dfa47b6201 Don't proxy certain headers across the Yalt boundary as it can mess-up proxied local requests 2023-12-18 10:52:31 +00:00
Eric Allam c01138b56e Fix pnpm lock file for release 2023-12-14 17:27:45 +00:00
github-actions[bot] 70fdedcea5 chore: Update version for release (#796)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-14 17:27:08 +00:00
Matt Aitken 49ca4c4430 Fix for #731, integrations getting stuck if OAuth then moved to API key (#732)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 1s
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-12-14 15:58:50 +00:00
Eric Allam 812b5afe6f Add support for the built-in Trigger.dev tunnel for local development (#795)
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 3s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 13s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Add support for the built-in Trigger.dev tunnel for local development

* Remove the test script from yalt

* Add to the README

* Improve the outdated packages warning
2023-12-14 15:33:56 +00:00
Matt Aitken 922520cefb Fix for the overage estimation on the usage page always being zero
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 4s
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-12-13 18:57:14 +00:00
Matt Aitken 4bd1786ae5 Billing (#789)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 5s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Initial commit - started work on new billing components

* Added a slider component

* Added features to the pricing tiers

* Small pricing tier margin tweaks

* WIP on a concurrecy chart

* Reworked the pricing tiers to include a segmented controller and tooltips

* Renamed the charts storybook page

* Made the way data is added more flexible and added some definition tool tips

* Term definitions are used properly in the tiers

* Callouts can now have an optional CTA on the right hand side

* Alignment fix for the callouts

* organize imports

* Definition tooltip now its own component

* renamed the storybook story

* WIP new volume discount table and usage sliders

* Added pricing calculator sliders

* Fixed alignment of the legend

* Breadcrumb now has an upgrade prompt and button

* New Join our Slack button in the side menu

* New progress meter in the side menu

* Use the highest of 2 values to show progress

* An attempt to fix the step count in the calculator slider

* WIP usage progress bar

* Added the 4 progress bars

* More examples of the usage bar

* Better way to include the percentage in the free plan progress meter

* The usage bar now works with the extra runs over the free limit

* Pricing calculator has better slider logic

* Moved the free plan usage bar into it’s own component and added it to storybook

* Usage bar chart now supports a paying customer option and optional billing limit. Also added more usage examples to storybook

* Added more examples of usage to storybook

* tooltip takes classname

* Format numbers nicely

* Added a tooltip to show the precise numbers in the chart

* small improvements to the billing calculator

* New onboarding choose plan page

* pricing tiers better fill the size of their container

* Removed unused code

* Usage bars animate

* Wording tweak

* Callouts fit the button size better

* Added new routes for the 2 new billing pages

* import cleanup

* Added meta info in the header for bill price, plan type and billing period

* made free a noun

* Added pricing calculator to the plans page

* Fixed some illegal markup when using tooltips

* Added container query support

* Added new concurrency chart to the usage page

* billing now has a green theme

* Simplified the plan summary info int the header

* Fixed padding alignment

* Use a custom lable for the concurrent runs chart

* Removed the Job runs table

* Latest lockfile

* Show a message if you haven’t done runs yet

* Added a layoutId to the pageTabs

* Show a message callout if you’ve exceeeded 10k runs on the free plan

* Added a callout on the plans page if you’re over the runs limit

* Fixed button inside button bug

* Removed the background gradients from the app

* The billing package is importing properly

* Getting the curent plan for an org

* Reading the current plan and usage

* Hooked up the free plan bar

* Render basic billing details

* vol discount table has optional values

* Added a new page to show new subscribers

* Created a new hook for confetti on the subscribed page

* Toast styling updated

* The Invoice and Manage card details links are working

* Meta appEnv data optional

* Data for the plans page

* Format the billing period duration in days

* Switch to 20 icons

* URL for the subscribed page now includes the org path

* New pathBuilder path for the subscribed page

* Plans are upgraded/downgraded successfully

* Fixed badly named paths

* Improved some of the display

* Deal with when the user has canceled so they can re-upgrade

* Subscribing from Stripe is working, and canceling

* Tidied up some bits, latest billing package

* The tiers are now rendering using the real data

* The onboarding screen is hooked up, but not linked to yet

* Onboarding price selection working

* Pricing slider working

* Price estimation working

* Pricing calculator working on the select a plan page

* Button copy change

* Improved the layout of the run calculator marker

* Fix for the period end when you’ve canceled

* Improved the formatting in the calculator

* Pricing table tooltip uses the vol discount pricing table

* Remove the vertical lines from the calculator

* Contact us button in Enterprise tier opens the contact us form

* Added composite index to triggerdotdev_events.run_executions for event_time and organization_id

* Concurrent run chart data

* Improves styling, fix for React error with Enterprise contact button

* Show warning box when you’ve hit the concurrency in the past 30 days

* Lots of work o the usage page

* Improvements to the usage page

* Show 31 days of data, fix for not showing the current date…

* Stripe portal links are generated when the user clicks through

* Better alignment of the reference line and x-axis label

* Readme update

* Fixed pricing button loading buttons

* Show warnings about concurrency and runs on the plans page

* Removed some storybook stories

* Join Slack channel shows if you’re subscribed with instructions

* Added a gap between the runs charts

* Improved the definitions

* Added some margin to the page loading spinner

* A wider, cleaner feedback panel

* Modal backgrounds match the Sheet style

* Fixed menu item text being clipped

* Improved icons for execution time and exclusion count

* Concurrency chart now renders the dates nicely

* Fix for plans data on the plans page

* The healthcheck doesn’t need to do a HEAD request to /

* Better disabled states and fixed the disabled hover state issue

* Improved the segmented controller style

* loading spinner now centered inside the button

* Redirect to the project page when selecting the free plan

* Fix for “Runs” and added real date to upgrade warning

* DeCAPITALIZED some things

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-12-13 12:48:26 +00:00
github-actions[bot] daae6df428 chore: Update version for release (#794)
* chore: Update version for release

* Update lockfile

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2023-12-13 10:23:36 +00:00
nicktrn fc31c6c07a Fix: Test package run isolation (#793)
* Fix link

* Move paths to compiler options

* Add failing test case

* Fix test isolation

* Add changeset
2023-12-13 09:49:52 +00:00
Eric Allam 6d4676f204 Auto update endpoint URL on redirect (#791) 2023-12-12 13:14:25 +00:00
Andreas Thomas 0a51ef3a06 fix: typo (#788) 2023-12-10 22:24:01 +00:00
Eric Allam 772f1b41a0 Fix pnpm lock file 2023-12-08 10:20:32 +00:00
github-actions[bot] d42ba80108 chore: Update version for release (#785)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-08 10:19:57 +00:00
Eric Allam 6a3c563f14 Fixed Job.attachToClient 2023-12-08 10:15:06 +00:00
Eric Allam c04cdfde8c Fix pnpm lock file 2023-12-07 13:00:32 +00:00
github-actions[bot] 750c1ff1e3 chore: Update version for release (#783)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-07 12:59:53 +00:00
Eric Allam 0f9f010206 @trigger.dev/airtable: Removed named imports (#782) 2023-12-07 12:53:58 +00:00
Eric Allam 11f8ff1bb4 Fixing endless retrying of non-successful run executions
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-12-06 21:01:41 +00:00
Eric Allam 85b3352764 Fix pnpm lock file 2023-12-06 20:05:49 +00:00
github-actions[bot] 9fb8dceef4 chore: Update version for release (#781)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-06 20:05:29 +00:00
Eric Allam 0c14e4cdfe Stop importing subpaths in @trigger.dev/airtable (#780) 2023-12-06 20:02:37 +00:00
D-K-P 2099c6308e Latest lock file 2023-12-06 15:45:43 +00:00
github-actions[bot] 08e6cad28a chore: Update version for release (#779)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-06 15:42:36 +00:00
Eric Allam f3efcc0c28 Moved Logger to core-backend, no longer importing node:buffer in core/react (#778) 2023-12-06 15:37:50 +00:00
Eric Allam 37ef335b66 Resolve pnpm lock file 2023-12-06 11:29:58 +00:00
github-actions[bot] 33d555d00e chore: Update version for release (#776)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-06 11:28:06 +00:00
Eric Allam 17f6f29d05 Feature: Support multiple runtimes other than Node.js (#774)
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 5s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-12-06 10:23:37 +00:00
Eric Allam 08f7c639ef Maximum background fetch timeout is now 5 minutes
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-12-04 15:02:15 +00:00
github-actions[bot] 1567239718 chore: Update version for release (#771)
* chore: Update version for release

* Release lockfile

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2023-12-04 14:14:41 +00:00
nicktrn de652c1dfb Fix: Shopify tasks and KV get() return types (#770)
* Improve KV get types

* Fix shopify find and all task return types

* Add changeset
2023-12-04 12:37:55 +00:00
Eric Allam 1f3733b70f Fix resuming timed out or auto-yielded executions
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / units (push) Failing after 21s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 21s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-12-04 09:20:40 +00:00
Dan b5aea6c534 Replaced showcase links -> api (#764)
* Updated the examples section with new links / changed showcase link to project showcase

* Added links to the api pages for all of our integrations

* Deleted card from the airtable tasks page
2023-12-01 17:35:54 +00:00
Eric Allam 0769dc4315 Fixed the event names for stripe docs 2023-12-01 08:42:05 +00:00
Eric Allam 5d00fc7cdb Update pnpm lock file for release 2023-11-30 21:54:43 +00:00
github-actions[bot] 00b0c3e02e chore: Update version for release (#765)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-30 21:54:13 +00:00
Eric Allam 7e3a82ef47 Issue #747: Add Stripe invoice triggers 2023-11-30 21:48:45 +00:00
James Ritchie 5dda6cd16c Added the border to the page header on the Runs page
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 3s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 21s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-11-30 10:32:38 +00:00
Matt Aitken 76b7fb2337 The status API endpoints return only the original set of statuses 2023-11-30 10:31:27 +00:00
Matt Aitken 68cbfd8d23 Log out the run status 2023-11-30 10:29:48 +00:00
Eric Allam 41a49f6bb2 Make the run list query more efficient
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-11-29 19:28:15 +00:00
Eric Allam 2f755158b4 Updated pnpm lock file 2023-11-29 11:25:27 +00:00
github-actions[bot] 419b93809d chore: Update version for release (#757)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-29 11:24:58 +00:00
Eric Allam bd4bc51daa Update plain (#762)
* updated the plain SDK

* Updated the action to support the new Threads SDK

* latest lock file

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-11-29 10:58:06 +00:00
Eric Allam ff540c9e4a Revert "Update plain (#761)"
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 3s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 5s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
This reverts commit 3f217ff3e0.
2023-11-29 10:51:13 +00:00
Eric Allam 3f217ff3e0 Update plain (#761)
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* updated the plain SDK

* Updated the action to support the new Threads SDK

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-11-29 10:49:37 +00:00
Eric Allam 9cb39bf7d7 Added Redis example env vars 2023-11-29 10:34:54 +00:00
Eric Allam ca05d5f603 @trigger.dev/openai: Adding additional assistant tasks 2023-11-28 22:48:31 +00:00
Hemachandar 4dc46cbbe4 fix: transform Headers object to Record type (#760) 2023-11-28 18:42:50 +00:00
nicktrn 1dcd87a2aa URI encode keys (#758) 2023-11-28 18:05:15 +00:00
Eric Allam c4cb98af5c Clarified the run execution limits 2023-11-28 16:23:35 +00:00
Eric Allam 6ebd435e81 Feature: Run execution concurrency limits (#750)
* WIP execution concurrency controls implemented via Redis

- Split up resuming a run and executing a run
- Added some new statuses to better show what is going on in a run
- Removed preprocessing runs

* WIP

* Convert to using ZSETs and adding env vars

* Removed unused import

* Improve run number generation using advistory locks, and only on start

* More execution concurrency stuff

* Add support for job concurrency limits and concurrency limit groups

* Create wild-swans-battle.md

* Increase slots refresh timeout to 10s

* Try to fix Redis connection issues

* Don’t be so strict about the APP_ENV

* Add the blank tls option to the normal redis client as well

* Add docs
2023-11-28 16:21:06 +00:00
Eric Allam caf203c084 Made webhook index stats optional
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 4s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 19s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-11-28 15:04:24 +00:00
Eric Allam dbc2e3f713 Refreshed pnpm lock 2023-11-28 14:53:37 +00:00
github-actions[bot] a5b49cb61f chore: Update version for release (#753)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-28 14:53:06 +00:00
nicktrn 096151c014 Fix: Shopify subpath imports, enhance docs (#755)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Fix invalid subpath imports

* Remove @trigger.dev/tsup dep

* Update lockfile

* Small github docs fix

* Pull runtime adaptor import out of integration

* Fix io.store docs examples

* Add sendEvents to task library

* Add KV to task library and small fixes

* Suppress duplicate http endpoint warnings

* Changeset

* Add task returns
2023-11-27 16:03:03 +00:00
Dan 5d28f1a3d6 Update github-triggers.mdx 2023-11-27 10:32:21 +00:00
Eric Allam 56ae0ed0d3 Only tag v.docker tags with the latest tag 2023-11-24 16:46:06 +00:00
Eric Allam a2f5dc8bc6 Only attempt to login to Docker if the workflow event is a push 2023-11-24 14:16:41 +00:00
Eric Allam d56b2eded5 Features: new login page (#754)
* Initial commit and WIP new login page

* added wrapper to fix layout

* Improved the layout and style of the page

* Added a new extra-large button style (and added it to storybook)

* Moved the cusom onboarding style tooltip to the Tooltip component

* Populated the bento grid with items

* Imrpoved the quote styling

* Tooltip doesn’t stay if you mouse over it

* Added more content including a list of quote that cycle on page load

* New quote mark positioning

* Added the rest of the content to the tooltips

* Made the bento grid responsive

* Moved the login page layout to a seaparte component

* Login pages now use the new layout component

* Extra large button style tweak

* Match the large input style with the large button style

* Cleaned up imports and made the Ts & Cs text the same for magic links

* Layout fix

* Quotes don’t push content off the screen as much

* Added openai integration to the grid

* Make the quote fit if the screen height is narrower

* Style update to the magic link sent state

* Icon colour updates

* fixed text centering on mobile

* Icons are more responsive

* removed unnecessary w-fulls

* Added 2 onboarding questions

* Revalidate form on submit, not input

* Typo fixes

* Name field focuses by default

* Added conform to the employees question

* Update pnpm lock file

* Implement referral source and company size on managed cloud

* Fixed some type errors

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-11-24 14:08:50 +00:00
nicktrn 0ae9adafbf Fix: Shopify build and webhook indexing (#752)
* Revert to old tsup config

* Make webhooks index optional
2023-11-24 11:45:33 +00:00
Eric Allam 03403ad9cb Fix for pull request workflow (#751)
* Allow PRs that have /publish-docker in the body to cause docker images to be published

* Also ignore mdx file changes

* pr_checks now uses pull_request instead of pull_request_target
2023-11-24 11:19:06 +00:00
nicktrn 067e19fec9 Feature: New Webhook Triggers, KV Store, and Shopify integration (#745)
* Attach webhook to http endpoint

* Create internal webhook handler job

* Fix delivery

* Add webhook trigger

* Index webhooks

* Fix webhook connect

* Webhook activation

* Rename to TriggerWebhook

* Save webhooks to index

* Extend register payload

* Revert attachSource

* Add update webhook helper

* Use crud interface for registration

* Remove filter from params

* Destructure options

* Missing ohash dep

* Chainable trigger filter

* Optional request verification

* Handler call with context

* Update config on register success

* Rename to webhookData

* Add basic key-value store

* Improve kv store

* Webhook trigger UI

* Webhook delivery UI

* Some fixes

* Airtable experiments

* Fix tabs transitions

* Only run register if config differs

* Use subtasks for delivery

* Missing dep

* Error handing and fixes

* Namespace cursor

* Fix hmac verification

* Gut Airtable

* Lockfile

* Template optional api key

* Template event sources and icons

* Template remove filter from params

* Fix models template

* Template fixme

* Fix webhook trigger header

* Actually send params for registration..

* Add back oauthed client

* Verify signature header encoding param

* Fix delivery context

* Webhook create retry with prior delete

* Webhook source type fixes

* Webhook config merge

* Update webhook icon

* Verify webhook task options

* Rename handler to generateEvents

* Send event icons

* Shopify with webhooks

* Shopify job-catalog entry

* Lockfile

* Add webhook migration

* Better icons

* Type fixes

* Make param type optional

* Scope type

* More examples

* More schemas

* Trigger catalog

* Bump versions

* Just a few more events they said

* Simplify session and config

* Typed serializer

* Fix payload type

* Remove unused getters

* Reduce logging output

* Rest resource tasks

* Lockfile

* Bump version

* Remaining triggers

* Fix event types

* Fix import

* Link from webhook trigger to http endpoint

* Use rest resource tasks for crud

* Fix rest save type

* Improve KV types

* Improve attach webhook logging

* Make header schema more lenient

* Fix save type again

* Final error callback

* Shorten keys

* Remove ohash diff

* Disable Airtable webhooks

* Webhook environments

* Fix shopify error import

* Link to integration and http endpoint

* Integration catalog entry

* Disable oauth

* Require api key, simplify client secret

* Move trigger types

* Update catalog example

* Small KV endpoint refactor

* Fix custom migration

* Webhook environment migration

* Improve crud types

* Remove unused schema

* Fix crud context type

* KV limits and HTTP verbs

* KV improvements

* Jobless webhook delivery

* Airtable delivery fixes

* Make deliveries sexy again

* Some docs

* Fix payload types.. again

* Final payload pass

* Schema cleanup

* Refactor handler service

* Remove more stale schemas

* registerJobNamespace import

* Expose KV on TriggerClient

* Remove dummy click handler

* Remove unused components

* Fix delivery pagination

* Enable registration rerun button

* Comment out registration route action handler

* Rename KV as not tied to IO anymore

* Swap api with trigger client

* Disabled fields param

* KV docs

* Changeset

* Shopify docs fixes
2023-11-24 10:22:12 +00:00
nicktrn 2cce68b5f7 Revert "Remove the pg listen code to see if it’s causing DB issues" (#749)
This reverts commit e8e7c116d1.
2023-11-24 09:30:48 +00:00
Dan e3c3aa91e1 Added Clerk / Cal.com / HubSpot / Hugging Face / Mailgun / Novu / WhatsApp HTTP endpoint examples (#748)
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-23 16:11:34 +00:00
Marius Vilholm 5c0ca83852 Fix wrong import in example for useRunDetails (#744)
Thanks @mariusvilholm
2023-11-23 15:35:33 +00:00
Marius Vilholm 2b24104c97 Fix wrong import in example of useEventRunDetails (#743) 2023-11-23 15:35:02 +00:00
Marius Vilholm 5977a5aa51 Fix spelling in OpenAI Assistants (#742)
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-23 15:34:30 +00:00
Eric Allam 131d0dbea4 Adding an example of using OpenAI GPT 4 Vision 2023-11-21 10:23:11 +00:00
Eric Allam 59277731c4 Fix pnpm lock file 2023-11-16 16:32:14 +00:00
github-actions[bot] 742d16087b chore: Update version for release (#739)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-16 16:31:41 +00:00
Eric Allam 0ab51d62bc Don’t send notifications for internal runs and add isTest to the notification
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 19s
🚀 Publish Trigger.dev Docker / units (push) Failing after 3s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 6s
2023-11-16 14:45:39 +00:00
Eric Allam 756024da78 Add support for Run Notifications #401 (#738)
* Add support for Run Notifications #401

* Fixed typo and added changeset
2023-11-16 14:19:42 +00:00
Dan c47ad5e038 Docs improvements and added API catalog links (#737)
* integration docs various improvements

* Added optional comments to step by step guide

* Added API catalog card to the integrations intro

* Added Api catalog button to the examples job tab

* Tweaked copy of api catalog card

* Update introduction.mdx

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-16 11:50:26 +00:00
Dan 888344797e Added code examples and modified blank state on the integrations & APIs page (#736)
* Added more API icons

* Updated the title

* Added resources.codeexample.ts to pull in example code from github

* Added regex to remove redundant express code from the examples

* Added cal examples

* Code changes onSubmit

* Improved the code loading state

* Removed request integration button

* Improved the Customhelp copy

* Fixed styling issues and removed imports

* Added all the code examples

* Don’t show title if there’s 1 or less examples

* Updated copy for the non example version

* Added example code to the apis without examples, added a focus state

* Fixed titles

* Got rid of the phantom gap

* Made the copy more generic so it works on the integration pages as well

* Removed import

* Added view examples button to the integration pages

* Removed fetch/sdk option from integration sheet

* Apply code example regex on the server

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-15 17:38:24 +00:00
Eric Allam 4a7caa68b6 Record when run executions start and finish
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-11-15 15:18:32 +00:00
Matt Aitken 3bc54d36f7 SQS logging: fixed error and message, plus using tracing 2023-11-15 13:32:40 +00:00
Matt Aitken 5295a3f798 Changed SQS error callbacks to log to error 2023-11-15 13:26:57 +00:00
Eric Allam 14429b8b66 Fix “transaction already closed” error when running tasks (#735) 2023-11-14 23:22:34 +00:00
Matt Aitken 5dd1bcc589 sendEvents proxy uses batches of 10 (max for SQS) 2023-11-14 21:34:08 +00:00
Matt Aitken ed5ee16de5 Better errors now get to users of sendEvent 2023-11-14 21:33:47 +00:00
Matt Aitken 076185d19f Added a perf test for sendEvents 2023-11-14 21:32:46 +00:00
Matt Aitken 1e07964d81 The parallel perf test is now driven by numbers 2023-11-14 15:46:34 +00:00
Matt Aitken f406e59c45 Configuration and docs for deploying the proxy to Cloudflare 2023-11-14 14:04:42 +00:00
Eric Allam 75550f535c Fix undefined task outputs
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 19s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
And also fix the SSE connection getting reset on a infinite loop when runs are complete
2023-11-13 16:18:41 +00:00
Matt Aitken 2b48e6d04b Feature: sendEvent can use an AWS SQS queue to increase reliability and throughput (#729)
* Initial commit (by create-cloudflare CLI)

* Changed the prettier rules

* Started writing Readme and got dev working

* Validate the request, parse the event and respond with the correct format

* Use ulidx instead of ulid in core

* Removed some boilerplate and use new function for sendEvent

* Removed old ulid package

* When handling a webhook, it’s not an error if the HTTP endpoint environment isn’t found

* If the env vars aren’t set, log the request and pass it through

* Pass requests through. Setup the env vars

* Flipped the API key detection because we can enforce that it’s the shape of a private API key

* Progress with sendEvent proxy

* Events are being put on the SQS queue

* The SQS event queue is working

* Make sure there’s a timestamp before enqueuing the evnet

* Log the sqs_event, not API key

* Added a bit more detail to the readme

* Improved the Cloudflare logs

* We don’t need the global.window hack for AWS, just globalThis

* Removed commented out wrangler.toml values

* Reworked the proxy to make it easier to add more endpoints

* Use json utility to send Responses

* Start work on proxying bulk events
2023-11-13 15:08:01 +00:00
Matt Aitken fe14947bb0 Removed waitForEvent beta warning. Made a snippet so we can use this in future easily. 2023-11-10 18:24:48 +00:00
Matt Aitken 0afa119721 Use node-fetch for invokeEphemeralEvents instead of @whatwg-node/fetch (#734)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-11-10 17:36:21 +00:00
Eric Allam af57bc208a Update pnpm lock file 2023-11-10 16:54:06 +00:00
Eric Allam 7cbbb26038 Merge pull request #730 from triggerdotdev/changeset-release/main
chore: Update version for release
2023-11-10 16:49:47 +00:00
github-actions[bot] 2e5f8d8de3 chore: Update version for release 2023-11-10 16:05:39 +00:00
Eric Allam 9a7c08c26a Improvements: Fix dangling SSE issue and compression memory leak (#733)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Downgrade to remix-auth-email-link to remove yarn dependency

* Turn off the pg listen service for now

* Add snapshot admin route

* A couple logger fixes

* Add ability to disable compression

* Add ability to disable SSE

* Fix SSE memory leak + DB load issue
2023-11-10 16:02:53 +00:00
Eric Allam e8e7c116d1 Remove the pg listen code to see if it’s causing DB issues 2023-11-09 21:52:48 +00:00
Matt Aitken 99dd6673f9 Test page display a message if there are no environments that the logged in user can run tests with 2023-11-09 20:12:20 +00:00
Matt Aitken a41d9b3e67 Fix for first endpoint sheet React error 2023-11-09 20:11:36 +00:00
Eric Allam cb1825bfaf Add OpenAI support for 4.16.0 (#726)
* Add OpenAI support for 4.16.0

* Add support for background polling and use that in OpenAI integration to power assistants

* Much improved OpenAI docs

* Added backgroundPoll docs

* Implements waitForEvent and added docs for more built in tasks

* Add sendEvent API referenc

* Write the task libray

* Add changeset and warning for waitForEvent
2023-11-09 16:59:58 +00:00
nicktrn d02173442c Feature: io.sendEvents() (#728)
* Add io.sendEvents

* Add examples to built-ins catalog entry

* Add docs

* Add changeset

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-09 16:42:23 +00:00
Matt Aitken 9f1f59cc81 Added the redirect to the invites page back in… 2023-11-09 16:30:21 +00:00
Matt Aitken e48c9b5e69 Made it clearer in the HTTP endpoints docs that we’re using Cal.com as an example 2023-11-08 10:15:42 +00:00
Eric Allam 55a9b96c88 Remove the coming soon warning 2023-11-07 16:31:44 +00:00
Matt Aitken dc8c8edf04 Latest lockfile 2023-11-07 16:27:31 +00:00
github-actions[bot] 41927edf12 chore: Update version for release (#718)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-07 16:26:41 +00:00
James Ritchie 1afba46cf0 Made the no-runs message consistent with the others
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-11-07 15:06:42 +00:00
James Ritchie 32e4fc6310 Updated our registered address on the magic link email 2023-11-07 14:57:04 +00:00
Matt Aitken 0add95e224 The auth method is now displayed correctly 2023-11-07 14:51:04 +00:00
Matt Aitken de7f133ef9 The Job List can shows jobs across projects now 2023-11-07 14:50:52 +00:00
Matt Aitken 8b13681afc Removed the project + from the sidemenu 2023-11-07 14:30:04 +00:00
Matt Aitken 88baac2144 Improved the side menu project styling 2023-11-07 13:42:56 +00:00
Matt Aitken df664ad472 Removed “New project” from the project selector. Added a + button to the Proejcts side menu 2023-11-07 12:46:41 +00:00
James Ritchie d8c53784f7 Triple dot menus now dismiss when you click an option 2023-11-07 11:45:53 +00:00
James Ritchie 3c7b2de095 Clearer state for unsupported frameworks 2023-11-07 11:45:53 +00:00
James Ritchie ae9249405c Autofocus input fields 2023-11-07 11:45:53 +00:00
James Ritchie d63f4e2451 User avatar now displays if one is set 2023-11-07 11:45:53 +00:00
Matt Aitken f4275e5054 Added a changeset for verifyRequestSignature – added an error if the passed in secret is undefined or empty 2023-11-07 10:54:08 +00:00
Matt Aitken 690b308fea verifyRequestSignature – added an error if the passed in secret is undefined or empty 2023-11-07 10:52:55 +00:00
Eric Allam a74716a1b9 Added waitForRequest built-in tasks (#725)
* Added waitForRequest built-in tasks

* Create grumpy-buttons-thank.md
2023-11-06 18:30:49 +00:00
Eric Allam 42185bc36b Upgrade resend integration to use resend-node 2.0.0 (#724)
* Upgrade resend integration to use resend-node 2.0.0, fixes issue with Next.js 14

* Create mighty-dodos-study.md
2023-11-06 18:30:37 +00:00
Eric Allam 276181b91c Fixed HttpSourceRequest import 2023-11-06 17:55:34 +00:00
Eric Allam 578d2e5410 Fixed Buffer reference error in the browser 2023-11-06 17:51:47 +00:00
Eric Allam 329944841f Fix the cancel event example in job ref 2023-11-06 17:38:18 +00:00
Eric Allam 4a0f030ebf Adding no-cache to our client fetch to fix Next.js POST caching 2023-11-06 17:38:18 +00:00
Matt Aitken 13345d0025 e2e failure was caused by… a space 2023-11-06 17:02:27 +00:00
Matt Aitken d905059915 e2e moving to project correctly 2023-11-06 16:36:05 +00:00
Matt Aitken 0d8530028f Added “test:e2e:ui” script so you can see Playwright running 2023-11-06 16:35:11 +00:00
Matt Aitken c37e7f3030 Fixed docs link 2023-11-06 16:34:33 +00:00
Matt Aitken 6d400c3b57 Fix for trigger link showing on non-http endpoint jobs 2023-11-06 16:34:12 +00:00
Matt Aitken 2b8a44aed1 Fix docs link to HTTP endpoints 2023-11-06 16:11:23 +00:00
Matt Aitken 6d842b3abb Added shortcodes to the e2e test suite 2023-11-06 15:54:55 +00:00
Matt Aitken 9c6838e5c8 HTTP endpoints, HTTP triggers and new sidebar (#720)
* Setup basic storybook page

* storybook preview pages don’t have padding

* Added a storybook story for the side menu

* WIP on a new side menu component

* New variant for the side menu button style

* Side menu items can now recieve a badge ‘count’

* Storybook side menu update

* Added a new font size

* you can now set the spacing between an icon and the text

* Moving the error and count badge to the right side of the button

* Added a menu sub item button style

* added sub items to the menu and all the rest of the items

* Proper background class used

* Added a side menu header and main org header

* Removed the no-mobile blocker overlay so you can use the site on a phone

* Better section name

* Created a cusom popover trigger that can take anything as children. Also the popover titles can now take a variant

* Added a popover for the user avatar menu item

* Removed the tool tip

* Arrow Popover can no also go full width

* neater conditiona;

* Removed unnessary div. And error icon can display an error message

* menu item buttons can take a variant

* Added placeholder items for the org dropdown

* add a max height to the org menu popover and handle scrollbars

* Added a border if the menu scrolls when you have lots of project items

* warning badges display tooltips if you give them some error text

* Removed the sticky header so the scrollbar looks better

* Fixed a layout issue from removing the sticky header

* Better spacing between lists

* Popover can take any react component icon and an icon classname

* Side menu headers now have a dropdown by clicking the ellipsis icon

* Side menu items can take an icon colour

* Updated styling for the dropdown Popover menus

* Installed Tabler icons

* Updated some icons to use Tabler

* Started defining the HttpTrigger class

* RequestFilterSchema is part of core

* Use the RequestFilterSchema from core

* EventSpecificationExample now derived from schema

* Need to define the class like EventTrigger

* Catalog reference that uses the httpTrigger

* The logic for the httpTrigger is in the separate file, just call it from the client using defineHttpTrigger

* Basic HttpTrigger

* Import fixes

* Transforming httpTrigger to the format we want

* Initial httpTrigger types working

* Added HTTP generic type

* Added some notes

* Rejigged some things to support verifying payloads

* Use new security function for verifying

* Added truncating text support if side menu names get long

* Renamed some stuff

* Removed test copy

* HttpEndpoin that isn’t a trigger

* Renamed hostname to source

* Created HttpEndpoint as a top-level concept, it produces triggers using onRequest

* Started working on registration of the httpEndpoints

* Removed the new TriggerMetadata for now, http will use static

* Added PayloadType to EventRecord. Default is JSON.

* The payload from an HTTPEndpoint is now a Request

* Initial TriggerHttpEndpoint migrations

* Initial server side work

* Migration to add shortcode to RuntimeEnvironment

* Added TriggerHttpEndpointEnvironment table

* Made the httptrigger catalog example runnable

* Progress creating/updating httpEndpoints

* HTTPEndpointEnvironment updating now works

* Added a note about EventRecord HttpEndpoint relationship

* When creating environments, create a shortcode

* Fix for secret not being retrieved correctly

* Request filtering

* Add DOM types to Core

* Early work dealing with a webhook arriving

* Added some todo notes to discuss tomorrow

* Added relations to the endpoint so we can call the user’s server

* Basic handling of immediate responses

* The immediate responses are now going back to the webhook provider

* Handling is now generating events and responding where relevant. Not yet matching filters and doing verification

* Added source to TriggerHttpEndpointEnvironment

* The event filter is now optional

* The source is passed through from the httpEndpointEnv

* Added optional payloadType when ingesting an event

* Added tabler

* Updated @remix-run/testing usage for remix 2

* Fixed org menu header width and added org and project types

* Added custom icons for Runs and Endpoints

* Updated the menu icons

* storybook side menu shows static project and org data

* Updated side menu

* Plumbed in paths

* Updated eventFilter to deal with undefined/null payloads

* Started work on filtering, but it’s not working

* Strict null

* async filtering it working

* Added environment shortcodes to the seedCloud script

* Don’t pass the secret back, added a verifyPayload function that gets called before runs start

* Fix for skipping triggering runs

* The verify callback is now passed through to the trigger so it can be called

* If verifying fails, have a reason which we can show to the user

* Don’t verify if it’s a test run

* Fixed the log in the example code

* Added loader and action to storybook preview

* Fix for svg attribute name issues in EndpointIcon

* Started work on the side menu

* This logic has moved to a separate file, in a different branch

* Don’t get the environments when getting orgs

* Lots of fixes for the new SideMenu

* Refactored some org/project things to fit the new structure better

* Some progress restructuring the app for the new sidebar

* More changes to data loading

* Deleted some old pages. The redirecting is working if you’re on the root but logged in

* Basic styling for the account page. We need a back button

* Fixes for the integrations pages

* Breadcrumb implemented

* Add a border to the bottom of the breadcrumb

* Feedback is working, not in Storybook

* Removed expandSidebar from the Handle

* Org level breadcrumbs

* Getting the project in the org route now works properly

* orgslug route before using the presenter

* More UI improvements

* Removed commented out line

* Use presenters for org and root

* Removed forceActive

* Split the project selector and user menu out

* The side menu items have correct active states

* Fixed the scrolling

* Put the org name in

* Improved the admin features

* Created the HTTP Endpoints list page

* Work on the HTTP endpoint page

* WIP on the HTTP Endpoint page

* Fix for response filter not showing

* Change page URLs to “http-endpoints” (from httpendpoints)

* Slugify the HTTP endpoint id

* Added todos for EventRecord associations

* Removed the Job/Trigger tab for now

* First draft of the help panels

* Rejigged the HTTP endpoint page so it splits out the deployed and not deployed environments

* Removed irrelevant columns

* Added a link to the deployment guide

* Removed the background image from the frameworks pages

Also…
- reordered the frameworks so supported ones come first in the list
- added logos to 3 framework pages that were missing them
- removed lots of un-used imports

* Fixed an issue where the breadcrumb could be hidden on small screens so broken the pages

The breadcrumb is always visible now

* Renamed ‘Onboarding’ to ‘Framework setup’

* Added a new http-endpoint icon to the side menu

* Side menu now has a nice looking scrollbar

* Scrollbar on the usage and billing page updated to the nice style

* Icon and colour updates to the side bar menu items

* Small copy improvement

* Fixed bug with the height of the org menu dropdown

* Page breadcrumbs now don’t include duplicate path links

* renamed uiBorder to ui-border

* Remove the padding from the small menu item if it’s used as a button

* Made the paragraph text a bit smaller

* EventRecord has relationships with TriggerHttpEndpoint/TriggerHttpEndpointEnvironment

* Improved the presenter used by the side menu. Also added the HTTP Endpoint badge count to the sidebar

* The dropdown org menu has the same badge as the side menu

* Improved the Projects page

* Fixed button style miss-match in the jobs table dropdown

* Made the popover dialogs support Headers and improved the regenerate API key popover

* Made the active status of a job more subtle

* Allow the Job to have a link on its Trigger

* Simplified loader because we ditched the Jobs menu. Also showing no runs help panel now

* Fixed type errors

* Pages that are org level can scroll if the content gets too long

* Fixed a redirect bug so you now redirect to the org page

* Updated the shortcode migration, to add additional letters to the end

* Removed the nav bar from the welcome onboarding page

* Only show the countBadge if the number is > 0

* Jobs icon now has a default colour the same as the nav menu

* Capital I for integration

* Clone the request before calling verify

* Separate the checks out so they can be easily logged

* Make the failure reason optional

* Made the verification error look better, if it’s provided

* Working WhatsApp example

* Can set the immediateResponseFilter to null in an update

* Deal with a null immediateResponseFilter

* Cal.com webhook working

* Header height reduced slightly

* Fixed button padding

* Removed un-used imports

* http endpoint table displays a message if you have no entries in the table

* Removed some duplicate text and swapped primary buttons for secondary style

* All ‘HTTP Endpoints’ now ‘HTTP endpoints’

* You have to use a RequestFilter when using HTTP Trigger

* Synchronous eventDispatcher filtering again. Better content-type check

* Stripe example

* Side menu “Usage & Billing” is just “Usage” if self-hosting

* Fix for filtering not working

* Removed filter log

* Fix for jobs being optional and null

* Removed old nav bar from the invite page

* Deleted old nav bar component

* import cleanup

* NoTask message only displays if the run has status “SUCCESS”

* Added a global page loading indicator

* Added documentation buttons to the most important pages

* Added GitHub

* Use “verifyRequestSignature” helpr

* Removed todo

* Fixed the layout of the Rerun Job popover

* Jobs table has margin bottom so it sits off the bottom of the page

* Small improvement to the search bar layout on the integration page

* HTTP endpoint and Trigger concept docs

* Reference docs and JSDocs for HTTP endpoint

* SDK reference docs for HttpEndpoint

* Added an HttpTrigger SDK reference, which mainly just point you elsewhere

* Improved the copy in the help panels

* Removed unused code

* Improved HTTP endpoint comment about setting the secret

* Delete unused asyncFilter

* Fixed the delete modal not having a title

* Always try get the arrayBuffer for an HTTP request, catch and use undefined as the default

* Add extra method to invokeTrigger

* Use a transaction for the secret store

* verifyRequestSignature docs page and JSDocs

* Added verifyRequestSignature docs page to nav

* Warn if they use the same HttpEndpoint id twice

* Added another job using the same HttpEndpoint

* Renamed the api route to http-endpoints from httpendpoints

* Debug bar for VSCode is now in the commandCenter

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
2023-11-06 15:32:40 +00:00
Abdou Belghalem 10e935b864 fix(TRI-1444): Allow only logins from WHITELISTED_EMAILS regex (fixes #685) (#721) 2023-11-06 09:06:54 +00:00
Eric Allam 02e453c3b5 Fix a ReferenceError when using OpenAI.backgroundCreateChatCompletion 2023-11-03 22:41:23 +00:00
James Ritchie 330ddb3311 NoTask message only displays if the run has status “SUCCESS” 2023-11-03 14:53:43 +00:00
Eric Allam 14a2f4cc94 Fix lock file 2023-11-03 11:26:16 +00:00
Alexandre Costa 7e57f1f3dd [TRI-1449] Display warning message when duplicate job IDs are detected (#699)
* add warning message when duplicate job IDs are detected

* fix warning message for duplicated job IDs in defineJob

* Create bright-buckets-bow.md

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
2023-11-03 11:10:26 +00:00
nicktrn cf8f994604 Feature: io.random() (#716)
* Add io.random

* Add changeset

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
2023-11-03 11:07:12 +00:00
Hemachandar 2ae5178479 fix: show only the current user's dev env in run-test page (#711) 2023-11-03 11:06:36 +00:00
Eric Allam 620b83832b Implement manually invokable jobs through the invokeTrigger (#700)
* Implement manually invokable jobs through the invokeTrigger

Also implemented a job run notification system, that will POST details of a run on completion. This combines with the task callbackUrl system to implement the invokeAndWait

* Document the invoke trigger

* batch invoke and wait

* background fetch timeouts

* Use @whatwg-node/fetch instead of the polyfilled fetch

* Fix some outdated dependencies in webapp

* Improved subtask error propogation messages

* Document the OpenAI changes and the batch invoke stuff

* Fix dequeuing jobs

* Don’t retry the OpenAI completion background task

* Added OpenAI changesets

* Use the new ResumeTaskService in ProcessCallbackTimeout as well
2023-11-03 11:05:00 +00:00
D-K-P c7434125e4 Removed Hacktoberfest from the readme 2023-11-01 15:12:01 +00:00
Eric Allam 67b5365d50 Fix pnpm lock 2023-11-01 10:16:52 +00:00
github-actions[bot] ea25c37a10 chore: Update version for release (#709)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-01 09:19:10 +00:00
Eric Allam c1710ae72d fix: Buffer import breaks @trigger.dev/react (#714) 2023-10-31 15:28:55 +00:00
nicktrn 9c5a7cf073 Fix: Allow errors and special values on Airtable field set (#697)
* Allow errors and special values on field set

* Add changeset
2023-10-31 10:17:05 +00:00
Eric Allam 8eedf994af Fixed call -> on 2023-10-30 21:58:04 +00:00
nicktrn 2c91c492df Feature: Cross-instance notification system (#689)
* Add notification system

* Graceful worker shutdown on migration notification

* Add notification catalog

* Rename pgListen call to on
2023-10-30 17:08:16 +00:00
Hemachandar b859abdb66 fix: remove unstable createRemixStub in storybook (#707) 2023-10-30 17:04:29 +00:00
James Ritchie 0876b87526 No Tasks in a Run warning message (#688)
* Card hover state is now grey

* Added a callout warning if you don’t have any tasks

* Conditionally show the no-task warning at the top of the run

* New job to test the no-task warning message

* Created a new example Job for the CLI that uses Tasks

* Updated the Next.js cli joke job

* Updated the Remix app cli job example

* Updated the Astro app cli job example

* Made sure the example job works in remix

* Added changeset

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
2023-10-30 16:24:46 +00:00
Hemachandar 9c4be40adc fix: append nanoid for dynamic trigger reg event (#628)
* fix: append nanoid for dynamic trigger reg event

* use idempotency-key as event-id

* add changeset
2023-10-30 16:24:03 +00:00
D-K-P 2635f4bd10 typo fix 2023-10-30 14:36:17 +00:00
nicktrn 401b7ee03d Set NODE_ENV to production in supabase docs 2023-10-28 13:09:01 +00:00
Eric Allam 6881de84ab Fix pnpm lock file 2023-10-26 20:04:03 +01:00
github-actions[bot] 0ca8296b9a chore: Update version for release (#686)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-26 20:03:37 +01:00
Eric Allam 55946fcdb7 Update @trigger.dev/nextjs to support Next.js 14 (#690) 2023-10-26 19:45:19 +01:00
Eric Allam c0f9f10b07 Upgrade to Remix v2.1.0 2023-10-26 15:54:21 +01:00
Eric Allam e80aa2826d @trigger.dev/express: Remove web-fetch dependency and use native node Request/Response 2023-10-26 13:42:20 +01:00
NIXEY 1be25d7639 BlankRow centered (#678) 2023-10-26 13:35:15 +01:00
ROMIT PAL 3c3a5e1f28 added a missing CODE OF CONDUCT file (#677)
Incorporating a Code of Conduct in your GitHub repository is crucial for promoting an inclusive, respectful community. It establishes guidelines, minimizes conflicts, safeguards participants, and adheres to legal standards. Additionally, it demonstrates professionalism and aligns with GitHub's best practices, contributing to a welcoming and harmonious environment, which, in turn, enhances collaboration and the project's overall success.
2023-10-26 13:30:14 +01:00
Vineet J Karni 8e741c7218 Added warning ⚠️ - No support for long-running servers (#669) 2023-10-26 13:18:28 +01:00
Hemachandar 83455e3ad4 feat: implement API key regeneration (#649) 2023-10-26 13:15:50 +01:00
Ronit Panda f0f0420280 feat: adds helm charts (#636)
* feat: adds helm charts template and adds postgres deps

* feat: adds all depl (barebones)

* feat: adds all depl (barebones)

* fix: removes print

* fix: image pulling issue

* fix: templatisation complete

* style: cleanup

* feat: works

* feat: adds ingress and some docs

* fix: ingress bug --> routing to cluster works

* feat: completes docs v1 todo --> needs hosting of the helm chart on chosen platform
2023-10-26 11:50:59 +01:00
Hemachandar c4533c36cb fix: set error message in runTask & executeJob (#634)
* fix: set error message in runTask & executeJob

* add changeset
2023-10-26 11:46:09 +01:00
Eric Allam f8e034c190 Fixed import that was causing typecheck to fail 2023-10-26 11:43:23 +01:00
Eric Allam 21f62e780c Added tests and refactored response to use camelCase 2023-10-26 11:30:49 +01:00
hmacr c3564ca0d4 fix doc 2023-10-26 11:18:11 +01:00
hmacr 6e1b8a11d4 feat: allow cancelling job runs for an event-id 2023-10-26 11:18:11 +01:00
Matt Aitken 9900f19b70 Migration: Updates any Endpoint runChunkExecutionLimits that were 60ms to be 60000ms 2023-10-25 15:50:11 +01:00
Matt Aitken 237545754f Indexing errors weren’t being serialized to JSON properly in some cases, so the errors weren’t getting inserted
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 21s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-10-24 18:08:46 +01:00
Eric Allam d7c1e98e5e Update pnpm lock 2023-10-24 17:49:39 +01:00
github-actions[bot] 71a0afeb8d chore: Update version for release (#684)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-24 17:49:06 +01:00
Eric Allam 36a390449a @trigger.dev/openai: Upgrade to openai-node SDK 4.13.0, add support for request options and defaultQuery/defaultHeaders 2023-10-24 17:45:43 +01:00
James Ritchie 375e495423 Fixed accounts table not scrolling in the admin area 2023-10-24 17:34:33 +01:00
D-K-P 866c20cd39 Updated the Slack underlying client example 2023-10-24 16:26:14 +01:00
Eric Allam 408785b8ef Update pnpm lock file 2023-10-24 13:41:35 +01:00
github-actions[bot] f982e00997 chore: Update version for release (#618)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-24 13:40:57 +01:00
Eric Allam 7e100b8362 ignore: adding more logs to perform task operation
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 6s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-10-24 12:14:52 +01:00
Eric Allam ea052c44c7 ignore: Add durationinMs to log 2023-10-24 11:38:35 +01:00
D-K-P 76224568f4 Airtable copy updates 2023-10-24 11:31:01 +01:00
Dan 72e67981b0 Merge pull request #666 from shelar1423/patch-3
Improved the Airtable integration documentation
2023-10-24 11:08:03 +01:00
Eric Allam cc44aff24b Added some docs about using the OpenAI integration as a universal client 2023-10-24 09:35:20 +01:00
Eric Allam 07d28ee4f5 Add custom baseURL and idempotency key support to OpenAI integration (#682)
This commit introduces the ability to set custom base URLs, which allows for the use of different APIs such as Perplexity. It also ensures that any OpenAI-related task now includes an idempotency key when making requests. Works for background tasks as well.
2023-10-24 09:19:38 +01:00
D-K-P f16a2cc4e6 Added link to the api reference repo to the readme 2023-10-24 09:11:57 +01:00
D-K-P b859837564 Edited the OpenAI ovrerview and added correct links for the tasks 2023-10-23 17:19:19 +01:00
Dan 1343003efa Merge pull request #673 from Rutam21/TRI-1430
[TRI-1430] : Improve the OpenAI integration documentation
2023-10-23 16:58:22 +01:00
Eric Allam 0c4aea0338 Add execution duration for background fetch 2023-10-23 16:55:00 +01:00
Eric Allam 627c767c64 fix: fix trigger sources getting repeatedly registered
Fixes a bug where trigger sources would get re-registered on every index, no matter if they were already properly registered or not. This was causing a lot of extra “internal” runs and being overly chatty with client endpoints
2023-10-23 15:24:35 +01:00
Digvijay Shelar f3a0c3a5d1 Merge branch 'main' into patch-3 2023-10-23 17:00:57 +05:30
dhselar1423 7bca3d9c15 made suggested changes 2023-10-23 16:57:52 +05:30
D-K-P 01fc1c6634 Slack integration docs copy updates 2023-10-23 12:17:29 +01:00
Dan 2d4f85e9f4 Merge pull request #667 from shelar1423/patch-4
Improved the Slack integration documentation
2023-10-23 12:08:40 +01:00
Rutam21 8718b28949 [TRI-1430] : Improve the OpenAI integration documentation 2023-10-21 04:55:12 +05:30
dhselar1423 2caa2a5bbd Merge branch 'patch-4' of https://github.com/shelar1423/trigger.dev into patch-4 2023-10-20 23:14:43 +05:30
dhselar1423 43a10ee695 slack fixes 2023-10-20 23:11:41 +05:30
Digvijay Shelar 018005d478 Merge branch 'main' into patch-4 2023-10-20 22:33:54 +05:30
dhselar1423 4da187c4c5 Merge branch 'patch-3' of https://github.com/shelar1423/trigger.dev into patch-3 2023-10-20 22:33:15 +05:30
dhselar1423 4925424778 Oauth and overview modifications 2023-10-20 22:32:48 +05:30
dhselar1423 7ef61222ed slack editing 2023-10-20 22:26:38 +05:30
Eric Allam 044d38e390 Improvement: Auto Execution Yielding (#612)
* Auto-yield run execution to help prevent duplicate task executions

* Add auto-yield config to endpoints

* Refactor run execution with buffer and limits

Introduced constants RUN_CHUNK_EXECUTION_BUFFER and MAX_RUN_CHUNK_EXECUTION_LIMIT. Adjusted PerformRunExecutionV2Service to use the new constants to fine-tune execution timings and buffers.

* Add endpoint probing functionality

Added new `RESPONSE_TIMEOUT_STATUS_CODES` in `consts.ts` to manage timeout responses. Additional functions `detectResponseIsTimeout(response: Response)` was added in `endpoint.server.ts` to detect if a response was a timeout based on the status codes from `RESPONSE_TIMEOUT_STATUS_CODES`.

Update actions to use new endpoint probing endpoint service. This allows for the early probing of endpoints to determine if they're up and running.

A new class `ProbeEndpointService` was created in `probeEndpoint.server.ts` which makes HTTP requests to a given endpoint and updates its properties based on the result.

Finally, `detectResponseIsTimeout(response)` is used in `performRunExecutionV2.server.ts` for marking the execution as succeeded when facing a timeout.

* Refactored probe method in EndpointApi class

The probe method of the EndpointApi class has been refactored to remove the error handling part and it now takes a timeout sent from the client directly. The corresponding changes were also made in the ProbeEndpointService and TriggerClient objects to reflect the alterations in the probe method.

The error handling related to the timeout has been removed and the responsibility of handling the timeout has been shifted to the client. Thus, the probe method has been greatly simplified. The `probeEndpoint.server.ts` file was also changed to accommodate the change in behavior of the probe result.

In the `triggerClient.ts` the timeout for probe is now read from the incoming request object. For backward compatibility, if no timeout is provided in the request, the default value of 15 minutes is used.

* Remove performRunExecution v1 enqueue function
* Better document limits and add docs on increasing function timeouts
* Upgrade webapp docker container to use 18.18.2
* force clients to yield when a run is executing in a gracefully shutting down worker
* Renamed task `key` to `cacheKey` and added more task documentation
* Index the `@trigger.dev/sdk` version on Endpoints
2023-10-20 17:44:10 +01:00
James Ritchie e7768881af Admin page now full screen with now extra UI 2023-10-20 16:53:04 +01:00
James Ritchie 5ea6a49d08 Made the app work very basically on mobile devices (#668)
* Removed the no-mobile blocker overlay so you can use the site on a phone

* Removed mobile dropdown from nav

* The app works on mobile at a width of 1024px
2023-10-20 16:12:17 +01:00
D-K-P dce5578327 Improved the GitHub overview copy 2023-10-20 14:35:28 +01:00
dhselar1423 fde3f17a25 fixed typos 2023-10-20 19:00:41 +05:30
dhselar1423 a6c9cac26c slack docs 2023-10-20 18:30:03 +05:30
Digvijay Shelar 960d99461a Merge branch 'triggerdotdev:main' into patch-3 2023-10-20 17:55:25 +05:30
dhselar1423 e5307133b3 airtable docs 2023-10-20 17:55:07 +05:30
D-K-P 34bb3acc23 Removed idempotencyKey line from GH tasks 2023-10-20 12:50:27 +01:00
Hemachandar 6d3b761cd7 fix: set correct next/prev cursors in run-list-presenter (#642)
* fix: set correct next/prev cursors in run-list-presenter

* set runs to return
2023-10-20 12:00:17 +01:00
Matt Aitken abc9737a4f Tabler icons added to NamedIcon
commit 13ed268f1d6732ef28c6e4a9eeb927c1af822032
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 11:38:22 2023 +0100

    Added all the tabler-icons to Storybook

commit 62738d71a40f17c45c0b29332359b2b670233a97
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 11:38:09 2023 +0100

    Use the no-stroke version of the tabler-sprite so we can control the stroke width

commit a1b7edd57f8b2d3f2a5f4d96cbc9eb0a887f3442
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 11:37:54 2023 +0100

    Set the default width to 1.5, so they’re a bit thinner

commit 69ba38802b849376611678537f7d8e248b60086b
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 10:55:34 2023 +0100

    Added a tabler icon to an events job catalog job

commit eb3c5f649d06adcc9ce0641f50e082ecdeead7a2
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 10:53:40 2023 +0100

    Moved the tabler sprite to the app and use an import. This means it’ll be cached and we can move it

commit c24d76e6df9233b001a0242f8806076562ca2e90
Author: Chaturved Degloorkar <48447253+chaturrved@users.noreply.github.com>
Date:   Fri Oct 20 15:22:59 2023 +0530

    feat: Add support for tabler-icons when using the icon for Tasks  (#629)

    * Add support for tabler-icons

    * Changed the core update type from minor to a patch

    ---------

    Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-20 11:41:45 +01:00
Bhargav Shirin Nalamati 84af64165c changed the title for contributors (#633) 2023-10-20 10:22:13 +01:00
Alexandre Costa 0adf41c7f0 Feat: add maxDuration setting for API/Trigger route in the CLI Init Command for Next.js (#617)
* add cli init maxDuration to the api/trigger route for nextjs

* refactor createTriggerRoute

* move boxen log to createTriggerRoute function

* refine detectNextVersion and versionNumberPattern regex to match the latest nextjs version

* add tests for the detectNextVersion function

* add changeset file

* If the regex doesn't match, it returns null instead of throwing

* Tweaked message about the max duration

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-20 10:16:44 +01:00
Dan b7f4e40edb Merge pull request #660 from Rutam21/TRI-1425
[TRI-1425] : Improve the Plain integration documentation.
2023-10-20 10:05:18 +01:00
Rutam21 cf33396dc0 [TRI-1425] Improve the Plain integration documentation 2023-10-20 02:45:46 +05:30
Dan 51f2cd6b3f Updated posthog to 1.83.0 (#608) 2023-10-19 16:34:41 +01:00
vimode 513d0f071d fix: broken links in documentation (#602)
* fix: broken links at what is triggerdev docs

* fix: broken links for apikey

* fix: broken sdk link

* fix: broken link in concepts/triggers

* fix: broken link for zod guide at concepts/triggers/events

* fix: broken links in title for projects

* Shouldn't have /docs at the start

* Use backgroundFetch path

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-19 16:27:21 +01:00
Hemachandar 50e3192453 feat: add ability to use custom tunnel in dev cmd (#597)
* feat: add ability to use custom tunnel in dev cmd

* Added a short flag -t for the tunnel flag

* skip framework url resolution if tunnel-url is provided

* remove type annotation

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-19 16:11:08 +01:00
Dan 1524c95a7a Merge pull request #650 from shelar1423/patch2
Get help icon and twitter text modifications
2023-10-19 12:46:41 +01:00
Eric Allam c8aaea8ad0 Improvements: Gracefully shutdown to prevent locked jobs (#648)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 6s
🚀 Publish Trigger.dev Docker / units (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 6s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* WIP

* Report ECS task info on startup and shutdown

* Fixed lifecycle name

* Re-add terminus

* Require the build dir when http server is disabled

* Remove unnecessary logs

* Implement graceful shutdown in ZodWorker

* Re-order some code

* Increase the keepAliveTimeout to 65 seconds to prevent LB 502 errors
2023-10-19 11:18:19 +01:00
dhselar1423 cf7f25abcd fix 2023-10-19 08:08:53 +05:30
dhselar1423 99453e8581 Merge branch 'patch2' of https://github.com/shelar1423/trigger.dev into patch2 2023-10-19 01:10:16 +05:30
dhselar1423 bc7206d981 x 2023-10-19 01:10:05 +05:30
Digvijay Shelar 64b8e150ea Merge branch 'triggerdotdev:main' into patch2 2023-10-19 01:03:19 +05:30
dhselar1423 b3b5852a88 fixed icons 2023-10-19 01:02:54 +05:30
Digvijay Shelar d1ecd6b99c framework icons (#635) 2023-10-17 22:00:56 +01:00
dhselar1423 0fb16a7324 framework icons 2023-10-18 01:21:47 +05:30
Matt Aitken 4f317055fd useDevEnvironment returns the current user’s dev environment, not the first 2023-10-16 16:19:16 +01:00
Eric Allam 3afd5f8cf3 Merge branch 'main' of github.com:triggerdotdev/trigger.dev 2023-10-16 16:04:09 +01:00
Eric Allam c922d24941 Adding simulate graphile job to easily debug graceful shutdown retries in docker image 2023-10-16 16:04:05 +01:00
Matt Aitken 39ec34f84f Latest lockfile 2023-10-16 13:16:02 +01:00
Ravan b9eba68078 feat: export base file (#610)
* feat: export base file

* Added a change set

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-13 17:44:05 +01:00
nicktrn 9df93d0798 CLI create-integration templates and shared configs (#511)
* Add tsup config package

* Add integration tsconfig to extend from

* Return correct version via cli -v

* Make create-integration use templates

* Add changeset

* Switch to liquidjs templates

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-13 16:55:22 +01:00
Matt Aitken 6769d6b439 Squashed commit of the following:
commit c028db229cece2bf7d7acf04e7b1c08fea61798c
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 13 16:04:54 2023 +0100

    Some fixes for getting the endpoint id

commit e31b7ac5f2d451f70e3d42a3fb3e5f718d93485b
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 13 15:39:18 2023 +0100

    Added a link in the instructions if Node isn’t detected

commit 1a59cced775fbeac3e5b94e0c151b490c838d8f8
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 13 15:39:05 2023 +0100

    Fixed some conflict issues

commit 77263ed26bcf7025071ac0faf15fb867fff245d0
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 13 15:38:56 2023 +0100

    Added a changeset

commit 1a7df3f37f0c65d68cb9a9fe7ac50328bbb4d879
Author: GuyBorderless <100129659+guy-borderless@users.noreply.github.com>
Date:   Fri Oct 13 17:31:01 2023 +0300

    feat: enable deno  (#531)

    * adding deno reference project and JSRuntime file

    * accessing runtime specific functions through JsRuntime

    * adding support for both deno.json and deno.jsonc

    * no automatic setup for deno projects currently

    ---------

    Co-authored-by: Guy Kedem <guy@propertiz.net>
    Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-13 16:20:18 +01:00
Eric Allam 70410d33af Ability to change the timeout of the graceful shutdown via env var 2023-10-13 15:12:05 +01:00
Hemachandar 6371dd4f95 feat: allow sendEvent to update an existing event (#607) 2023-10-13 14:46:35 +01:00
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
1105 changed files with 76967 additions and 13901 deletions
+2
View File
@@ -14,6 +14,8 @@
"ignore": [
"webapp",
"emails",
"proxy",
"yalt",
"@trigger.dev/database"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
+7
View File
@@ -12,7 +12,14 @@ APP_ENV=development
APP_ORIGIN=http://localhost:3030
NODE_ENV=development
# Redis is used for concurrency control
# REDIS_HOST="localhost"
# REDIS_PORT="6379"
# REDIS_TLS_DISABLED="true"
# OPTIONAL VARIABLES
# This is used for validating emails that are allowed to log in. Every email that do not match this regex will be rejected.
# WHITELISTED_EMAILS="authorized@yahoo\.com|authorized@gmail\.com"
# This is used for logging in via GitHub. You can leave these commented out if you don't want to use GitHub for authentication.
# AUTH_GITHUB_CLIENT_ID=
# AUTH_GITHUB_CLIENT_SECRET=
+66
View File
@@ -0,0 +1,66 @@
name: "🧪 E2E Tests"
on:
workflow_call:
jobs:
e2e:
name: "🧪 E2E Tests"
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: 🐳 Login to Docker Hub
if: github.event_name == 'push'
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
+32
View File
@@ -0,0 +1,32 @@
name: 🤖 PR Checks
on:
pull_request:
branches:
- main
paths-ignore:
- "**.md"
- "**.mdx"
- ".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
+84
View File
@@ -0,0 +1,84 @@
name: "🐳 Publish Docker"
on:
workflow_call:
jobs:
publish:
strategy:
fail-fast: true # when a job fails, all remaining ones will be cancelled
matrix:
runs-on: [buildjet-4vcpu-ubuntu-2204, buildjet-4vcpu-ubuntu-2204-arm]
name: ${{matrix.runs-on}}
runs-on: ${{matrix.runs-on}}
outputs:
version: ${{ steps.get_version.outputs.version }}
short_sha: ${{ steps.get_commit.outputs.sha_short }}
steps:
- name: 🐳 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: 🆚 Get the version
id: get_version
run: |
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
if [[ $GITHUB_REF == refs/tags/* ]]; then
if [[ $IMAGE_TAG == v.docker.* ]]; then
ORIGINAL_VERSION="${IMAGE_TAG#v.docker.}"
IMAGE_TAG="v${ORIGINAL_VERSION}"
elif [[ $IMAGE_TAG == build-* ]]; then
IMAGE_TAG="${IMAGE_TAG#build-}"
fi
echo "IMAGE_TAG=${IMAGE_TAG}"
elif [[ $GITHUB_REF == refs/heads/main ]]; then
# Handle main branch specifically
IMAGE_TAG="main"
echo "IMAGE_TAG=${IMAGE_TAG}"
else
echo "Invalid reference: ${GITHUB_REF}"
exit 1
fi
if [[ ${{matrix.runs-on}} == *-arm ]]; then
IMAGE_TAG="${IMAGE_TAG}-arm"
fi
echo "::set-output name=version::${IMAGE_TAG}"
- name: 🔢 Get the commit hash
id: get_commit
run: |
echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7)
- name: 🐳 Build Docker Image
run: |
docker build -t release_build_image -f ./docker/Dockerfile .
- name: 🐙 Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🐙 Push to GitHub Container Registry
run: |
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
env:
REGISTRY: ghcr.io/triggerdotdev
REPOSITORY: trigger.dev
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
- name: 🐙 Push 'latest' to GitHub Container Registry
if: startsWith(github.ref, 'refs/tags/v.docker')
run: |
LATEST=latest
if [[ ${{matrix.runs-on}} == *-arm ]]; then
LATEST="${LATEST}-arm"
fi
docker tag release_build_image $REGISTRY/$REPOSITORY:$LATEST
docker push $REGISTRY/$REPOSITORY:$LATEST
env:
REGISTRY: ghcr.io/triggerdotdev
REPOSITORY: trigger.dev
+11 -183
View File
@@ -4,9 +4,9 @@ on:
push:
branches:
- main
- improvements/*
tags:
- "v.docker.*"
- "build-*"
paths:
- ".github/workflows/publish.yml"
- "packages/**"
@@ -38,190 +38,18 @@ 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 ./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
uses: ./.github/workflows/e2e.yml
secrets: inherit
publish:
needs: [typecheck, unitTests, e2e]
runs-on: buildjet-4vcpu-ubuntu-2204
outputs:
version: ${{ steps.get_version.outputs.version }}
short_sha: ${{ steps.get_commit.outputs.sha_short }}
steps:
- name: 🐳 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: 🆚 Get the version
id: get_version
run: |
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
if [[ $GITHUB_REF == refs/tags/* ]]; then
if [[ $IMAGE_TAG == v.docker.* ]]; then
ORIGINAL_VERSION="${IMAGE_TAG#v.docker.}"
IMAGE_TAG="v${ORIGINAL_VERSION}"
fi
echo "IMAGE_TAG=${IMAGE_TAG}"
elif [[ $GITHUB_REF == refs/heads/improvements/* ]]; then
ORIGINAL_VERSION="${GITHUB_REF#refs/heads/improvements/}"
IMAGE_TAG="${ORIGINAL_VERSION}.rc"
echo "IMAGE_TAG=${IMAGE_TAG}"
elif [[ $GITHUB_REF == refs/heads/* ]]; then
IMAGE_TAG="${GITHUB_REF#refs/heads/}"
echo "IMAGE_TAG=${IMAGE_TAG}"
else
echo "Invalid reference: ${GITHUB_REF}"
exit 1
fi
echo "::set-output name=version::${IMAGE_TAG}"
- name: 🔢 Get the commit hash
id: get_commit
run: |
echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7)
- name: 🐳 Build Docker Image
run: |
docker build -t release_build_image -f ./docker/Dockerfile .
- name: 🐙 Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🐙 Push to GitHub Container Registry
run: |
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
env:
REGISTRY: ghcr.io/triggerdotdev
REPOSITORY: trigger.dev
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
- name: 🐙 Push 'latest' to GitHub Container Registry
if: startsWith(github.ref, 'refs/tags/')
run: |
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
docker push $REGISTRY/$REPOSITORY:latest
env:
REGISTRY: ghcr.io/triggerdotdev
REPOSITORY: trigger.dev
needs: [typecheck, units, e2e]
uses: ./.github/workflows/publish-docker.yml
secrets: inherit
+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
+40
View File
@@ -0,0 +1,40 @@
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: ⎔ Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: ⎔ Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.0.15"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: Run Unit Tests
run: |
pnpm run test
+2
View File
@@ -52,3 +52,5 @@ apps/**/public/build
/test-results/
/playwright-report/
/playwright/.cache/
.cosine
+1 -1
View File
@@ -1 +1 @@
v18.12.1
v18.18.0
+9
View File
@@ -0,0 +1,9 @@
{
"recommendations": [
"astro-build.astro-vscode",
"denoland.vscode-deno"
],
"unwantedRecommendations": [
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"deno.enablePaths": ["references/deno-reference", "runtime_tests/tests/deno"],
"debug.toolBarLocation": "commandCenter"
}
+23 -4
View File
@@ -32,7 +32,26 @@ Please follow the best-practice of adding changesets in the same commit as the c
!MAKE SURE TO UPDATE THE TAG IN THE INSTRUCTIONS BELOW!
1. Add changesets as usual `pnpm run changeset:add`
2. Create a snapshot version (replace "dev" with your tag) `pnpm exec changeset version --snapshot dev`
3. Build the packages: `pnpm run build --filter "@trigger.dev/*"`
4. Publish the snapshot (replace "dev" with your tag) `pnpm exec changeset publish --no-git-tag --snapshot --tag dev`
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
```
+43
View File
@@ -0,0 +1,43 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct that could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
+4
View File
@@ -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
+7 -1
View File
@@ -62,8 +62,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 +83,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).
## Meet the Amazing People Behind This Project 🚀
<a href="https://github.com/triggerdotdev/trigger.dev/graphs/contributors">
<img src="https://contrib.rocks/image?repo=triggerdotdev/trigger.dev" />
</a>
+7
View File
@@ -0,0 +1,7 @@
REWRITE_HOSTNAME=
AWS_SQS_ACCESS_KEY_ID=
AWS_SQS_SECRET_ACCESS_KEY=
AWS_SQS_QUEUE_URL=
AWS_SQS_REGION=
#optional
#REWRITE_PORT=
+13
View File
@@ -0,0 +1,13 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
+172
View File
@@ -0,0 +1,172 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
.dev.vars
.wrangler/
+11
View File
@@ -0,0 +1,11 @@
{
"semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
+72
View File
@@ -0,0 +1,72 @@
# proxy
## 0.0.11
### Patch Changes
- @trigger.dev/core@2.3.5
## 0.0.10
### Patch Changes
- @trigger.dev/core@2.3.4
## 0.0.9
### Patch Changes
- @trigger.dev/core@2.3.3
## 0.0.8
### Patch Changes
- @trigger.dev/core@2.3.2
## 0.0.7
### Patch Changes
- Updated dependencies [f3efcc0c]
- @trigger.dev/core@2.3.1
## 0.0.6
### Patch Changes
- Updated dependencies [17f6f29d]
- @trigger.dev/core@2.3.0
## 0.0.5
### Patch Changes
- @trigger.dev/core@2.2.11
## 0.0.4
### Patch Changes
- @trigger.dev/core@2.2.10
## 0.0.3
### Patch Changes
- Updated dependencies [6ebd435e]
- @trigger.dev/core@2.2.9
## 0.0.2
### Patch Changes
- Updated dependencies [067e19fe]
- @trigger.dev/core@2.2.8
## 0.0.1
### Patch Changes
- Updated dependencies [756024da]
- @trigger.dev/core@2.2.7
+68
View File
@@ -0,0 +1,68 @@
# Trigger.dev proxy
This is an optional module that can be used to proxy and queue requests to the Trigger.dev API.
## Why?
The Trigger.dev API is designed to be fast and reliable. However, if you have a lot of traffic, you may want to use this proxy to queue requests to the API. It intercepts some requests to the API and adds them to an AWS SQS queue, then the webapp can be setup to process the queue.
## Current features
- Intercepts `sendEvent` requests and adds them to an AWS SQS queue. The webapp then reads from the queue and creates the events.
## Setup
### Create an AWS SQS queue
In AWS you should create a new AWS SQS queue with appropriate security settings. You will need the queue URL for the next step.
### Environment variables
#### Cloudflare secrets
Locally you should copy the `.dev.var.example` file to `.dev.var` and fill in the values.
When deploying you should use `wrangler` (the Cloudflare CLI tool) to set secrets. Make sure you set the correct --env ("staging" or "prod")
```bash
wrangler secret put REWRITE_HOSTNAME --env staging
wrangler secret put AWS_SQS_ACCESS_KEY_ID --env staging
wrangler secret put AWS_SQS_SECRET_ACCESS_KEY --env staging
wrangler secret put AWS_SQS_QUEUE_URL --env staging
wrangler secret put AWS_SQS_REGION --env staging
```
You need to set your API CNAME entry to be proxied by Cloudflare. You can do this in the Cloudflare dashboard.
#### Webapp
These env vars also need setting in the webapp.
```bash
AWS_SQS_REGION
AWS_SQS_ACCESS_KEY_ID
AWS_SQS_SECRET_ACCESS_KEY
AWS_SQS_QUEUE_URL
AWS_SQS_BATCH_SIZE
```
## Deployment
Staging:
```bash
npx wrangler@latest deploy --route "<your-api-subdomain>/*" --env staging
```
Prod:
```bash
npx wrangler@latest deploy --route "<your-api-subdomain>/*" --env prod
```
## Development
Set the environment variables as described above.
1. `pnpm install`
2. `pnpm run dev --filter proxy`
+21
View File
@@ -0,0 +1,21 @@
{
"name": "proxy",
"version": "0.0.11",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230419.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
},
"dependencies": {
"@aws-sdk/client-sqs": "^3.445.0",
"@trigger.dev/core": "workspace:*",
"ulidx": "^2.2.1",
"zod": "3.22.3",
"zod-error": "1.5.0"
}
}
+20
View File
@@ -0,0 +1,20 @@
import { z } from "zod";
const AuthorizationHeaderSchema = z.string().regex(/^Bearer .+$/);
export function getApiKeyFromRequest(request: Request) {
const rawAuthorization = request.headers.get("Authorization");
const authorization = AuthorizationHeaderSchema.safeParse(rawAuthorization);
if (!authorization.success) {
return;
}
const apiKey = authorization.data.replace(/^Bearer /, "");
const type = isPrivateApiKey(apiKey) ? ("PRIVATE" as const) : ("PUBLIC" as const);
return { apiKey, type };
}
function isPrivateApiKey(key: string) {
return key.startsWith("tr_");
}
+87
View File
@@ -0,0 +1,87 @@
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
import { ApiEventLog, SendEventBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
import { Env } from "..";
import { getApiKeyFromRequest } from "../apikey";
import { json } from "../json";
import { calculateDeliverAt } from "./utils";
/** Adds the event to an AWS SQS queue, so it can be consumed from the main Trigger.dev API */
export async function queueEvent(request: Request, env: Env): Promise<Response> {
//check there's a private API key
const apiKeyResult = getApiKeyFromRequest(request);
if (!apiKeyResult || apiKeyResult.type !== "PRIVATE") {
return json(
{ error: "Invalid or Missing API key" },
{
status: 401,
}
);
}
//parse the request body
try {
const anyBody = await request.json();
const body = SendEventBodySchema.safeParse(anyBody);
if (!body.success) {
return json(
{ error: generateErrorMessage(body.error.issues) },
{
status: 422,
}
);
}
// The AWS SDK tries to use crypto from off of the window,
// so we need to trick it into finding it where it expects it
globalThis.global = globalThis;
const client = new SQSClient({
region: env.AWS_SQS_REGION,
credentials: {
accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
},
});
const timestamp = body.data.event.timestamp ?? new Date();
//add the event to the queue
const send = new SendMessageCommand({
// use wrangler secrets to provide this global variable
QueueUrl: env.AWS_SQS_QUEUE_URL,
MessageBody: JSON.stringify({
event: { ...body.data.event, timestamp },
options: body.data.options,
apiKey: apiKeyResult.apiKey,
}),
});
const queuedEvent = await client.send(send);
console.log("Queued event", queuedEvent);
//respond with the event
const event: ApiEventLog = {
id: body.data.event.id,
name: body.data.event.name,
payload: body.data.event.payload,
context: body.data.event.context,
timestamp,
deliverAt: calculateDeliverAt(body.data.options),
};
return json(event, {
status: 200,
});
} catch (e) {
console.error("queueEvent error", e);
return json(
{
error: `Failed to send event: ${e instanceof Error ? e.message : JSON.stringify(e)}`,
},
{
status: 422,
}
);
}
}
+112
View File
@@ -0,0 +1,112 @@
import { SQSClient, SendMessageBatchCommand } from "@aws-sdk/client-sqs";
import { ApiEventLog, SendBulkEventsBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
import { Env } from "..";
import { getApiKeyFromRequest } from "../apikey";
import { json } from "../json";
import { calculateDeliverAt } from "./utils";
/** Adds the event to an AWS SQS queue, so it can be consumed from the main Trigger.dev API */
export async function queueEvents(request: Request, env: Env): Promise<Response> {
//check there's a private API key
const apiKeyResult = getApiKeyFromRequest(request);
if (!apiKeyResult || apiKeyResult.type !== "PRIVATE") {
return json(
{ error: "Invalid or Missing API key" },
{
status: 401,
}
);
}
//parse the request body
try {
const anyBody = await request.json();
const body = SendBulkEventsBodySchema.safeParse(anyBody);
if (!body.success) {
return json(
{ error: generateErrorMessage(body.error.issues) },
{
status: 422,
}
);
}
// The AWS SDK tries to use crypto from off of the window,
// so we need to trick it into finding it where it expects it
globalThis.global = globalThis;
const client = new SQSClient({
region: env.AWS_SQS_REGION,
credentials: {
accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
},
});
const updatedEvents: ApiEventLog[] = body.data.events.map((event) => {
const timestamp = event.timestamp ?? new Date();
return {
...event,
payload: event.payload,
timestamp,
};
});
//divide updatedEvents into multiple batches of 10 (max size SQS accepts)
const batches: ApiEventLog[][] = [];
let currentBatch: ApiEventLog[] = [];
for (let i = 0; i < updatedEvents.length; i++) {
currentBatch.push(updatedEvents[i]);
if (currentBatch.length === 10) {
batches.push(currentBatch);
currentBatch = [];
}
}
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
//loop through the batches and send them
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
//add the event to the queue
const send = new SendMessageBatchCommand({
// use wrangler secrets to provide this global variable
QueueUrl: env.AWS_SQS_QUEUE_URL,
Entries: batch.map((event, index) => ({
Id: `event-${index}`,
MessageBody: JSON.stringify({
event,
options: body.data.options,
apiKey: apiKeyResult.apiKey,
}),
})),
});
const queuedEvent = await client.send(send);
console.log("Queued events", queuedEvent);
}
//respond with the events
const events: ApiEventLog[] = updatedEvents.map((event) => ({
...event,
payload: event.payload,
deliverAt: calculateDeliverAt(body.data.options),
}));
return json(events, {
status: 200,
});
} catch (e) {
console.error("queueEvents error", e);
return json(
{
error: `Failed to send events: ${e instanceof Error ? e.message : JSON.stringify(e)}`,
},
{
status: 422,
}
);
}
}
+15
View File
@@ -0,0 +1,15 @@
import { SendEventOptions } from "@trigger.dev/core";
export function calculateDeliverAt(options?: SendEventOptions) {
// If deliverAt is a string and a valid date, convert it to a Date object
if (options?.deliverAt) {
return options?.deliverAt;
}
// deliverAfter is the number of seconds to wait before delivering the event
if (options?.deliverAfter) {
return new Date(Date.now() + options.deliverAfter * 1000);
}
return undefined;
}
+67
View File
@@ -0,0 +1,67 @@
import { queueEvent } from "./events/queueEvent";
import { queueEvents } from "./events/queueEvents";
export interface Env {
/** The hostname needs to be changed to allow requests to pass to the Trigger.dev platform */
REWRITE_HOSTNAME: string;
REWRITE_PORT?: string;
AWS_SQS_ACCESS_KEY_ID: string;
AWS_SQS_SECRET_ACCESS_KEY: string;
AWS_SQS_QUEUE_URL: string;
AWS_SQS_REGION: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (!env.REWRITE_HOSTNAME) throw new Error("Missing REWRITE_HOSTNAME");
console.log("url", request.url);
if (!queueingIsEnabled(env)) {
console.log("Missing AWS credentials. Passing through to the origin.");
return redirectToOrigin(request, env);
}
const url = new URL(request.url);
switch (url.pathname) {
case "/api/v1/events": {
if (request.method === "POST") {
return queueEvent(request, env);
}
break;
}
case "/api/v1/events/bulk": {
if (request.method === "POST") {
return queueEvents(request, env);
}
break;
}
}
//the same request but with the hostname (and port) changed
return redirectToOrigin(request, env);
},
};
function redirectToOrigin(request: Request, env: Env) {
const newUrl = new URL(request.url);
newUrl.hostname = env.REWRITE_HOSTNAME;
newUrl.port = env.REWRITE_PORT || newUrl.port;
const requestInit: RequestInit = {
method: request.method,
headers: request.headers,
body: request.body,
};
console.log("rewritten url", newUrl.toString());
return fetch(newUrl.toString(), requestInit);
}
function queueingIsEnabled(env: Env) {
return (
env.AWS_SQS_ACCESS_KEY_ID &&
env.AWS_SQS_SECRET_ACCESS_KEY &&
env.AWS_SQS_QUEUE_URL &&
env.AWS_SQS_REGION
);
}
+13
View File
@@ -0,0 +1,13 @@
export function json(body: any, init?: ResponseInit) {
const headers = {
"content-type": "application/json",
...(init?.headers ?? {}),
};
const responseInit: ResponseInit = {
...(init ?? {}),
headers,
};
return new Response(JSON.stringify(body), responseInit);
}
+35
View File
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */,
"module": "es2022" /* Specify what module code is generated. */,
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"types": [
"@cloudflare/workers-types"
] /* Specify type package names to be included without being referenced in a source file. */,
"resolveJsonModule": true /* Enable importing .json files */,
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
"noEmit": true /* Disable emitting files from a compilation. */,
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"baseUrl": ".",
"paths": {
"@trigger.dev/core": ["../../packages/core/src/index"],
"@trigger.dev/core/*": ["../../packages/core/src/*"]
}
}
}
+7
View File
@@ -0,0 +1,7 @@
name = "proxy"
main = "src/index.ts"
compatibility_date = "2023-10-30"
compatibility_flags = [ "nodejs_compat" ]
[env.staging]
[env.prod]
+6 -3
View File
@@ -1,6 +1,6 @@
import type { Preview } from "@storybook/react";
import "../app/tailwind.css";
import { unstable_createRemixStub } from "@remix-run/testing";
import { createRemixStub } from "@remix-run/testing";
import React from "react";
import { LocaleContextProvider } from "../app/components/primitives/LocaleProvider";
import { OperatingSystemContextProvider } from "../app/components/primitives/OperatingSystemProvider";
@@ -23,13 +23,16 @@ const preview: Preview = {
},
],
},
layout: "fullscreen",
},
decorators: [
(Story) => {
const RemixStub = unstable_createRemixStub([
const RemixStub = createRemixStub([
{
path: "/*",
element: <Story />,
action: () => ({ redirect: "/" }),
loader: () => ({ redirect: "/" }),
Component: Story,
},
]);
@@ -0,0 +1,36 @@
export function EndpointIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.24336 5.11182C8.75159 4.60359 9.57559 4.60359 10.0838 5.11182L18.8882 13.9162C19.3964 14.4244 19.3964 15.2484 18.8882 15.7566L17.1469 17.4979C16.453 18.2196 15.6222 18.7956 14.7031 19.1922C13.7839 19.5889 12.7949 19.7982 11.7939 19.8081C10.7929 19.8179 9.79992 19.628 8.87318 19.2494C7.94643 18.8708 7.10449 18.3113 6.39662 17.6034C5.68875 16.8955 5.12916 16.0536 4.7506 15.1268C4.37205 14.2001 4.18212 13.2071 4.19194 12.2061C4.20176 11.2051 4.41112 10.2161 4.80779 9.29691C5.20445 8.37777 5.78044 7.54697 6.50206 6.85312L8.24336 5.11182Z"
fill="currentColor"
/>
<path
d="M3.32398 20.676L7.11974 16.8803"
stroke="currentColor"
strokeWidth="2.16549"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15.2535 3.32398L11.4578 7.11974"
stroke="currentColor"
strokeWidth="2.16549"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M20.676 8.74649L16.8803 12.5423"
stroke="currentColor"
strokeWidth="2.16549"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
@@ -0,0 +1,5 @@
import { LogoIcon } from "~/components/LogoIcon";
export function IntegrationIcon() {
return <LogoIcon className="h-3.5 w-3.5 flex-none pb-0.5" />;
}
+18
View File
@@ -0,0 +1,18 @@
export function RunsIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.2964 3.66962C11.97 3.83662 11.6961 4.09048 11.5047 4.40323C11.3134 4.71598 11.2121 5.07547 11.212 5.44211L11.211 8.0162L9.22047 8.0172C8.95637 8.0172 8.70309 8.12211 8.51635 8.30886C8.3296 8.4956 8.22469 8.74888 8.22469 9.01298V14.9877L8.23166 15.1042C8.26019 15.3464 8.37664 15.5697 8.5589 15.7318C8.74116 15.8939 8.97657 15.9834 9.22047 15.9834L11.211 15.9825L11.212 18.5585C11.2121 18.9524 11.329 19.3373 11.5478 19.6648C11.7666 19.9922 12.0777 20.2474 12.4415 20.3981C12.8054 20.5488 13.2057 20.5882 13.592 20.5114C13.9783 20.4346 14.3331 20.245 14.6116 19.9666L21.1698 13.4084C21.5432 13.0349 21.7529 12.5284 21.7529 12.0003C21.7529 11.4722 21.5432 10.9658 21.1698 10.5923L14.6116 4.03408C14.3331 3.75539 13.9782 3.56558 13.5918 3.48865C13.2054 3.41172 12.8048 3.45114 12.4408 3.60191L12.2964 3.66962Z"
fill="currentColor"
/>
<path
d="M3.24578 8.0172C3.48968 8.01723 3.72509 8.10677 3.90735 8.26885C4.08961 8.43092 4.20606 8.65425 4.23459 8.89647L4.24156 9.01298V14.9877C4.24128 15.2415 4.1441 15.4856 3.96986 15.6701C3.79563 15.8547 3.55751 15.9658 3.30414 15.9806C3.05077 15.9955 2.80128 15.9131 2.60666 15.7502C2.41203 15.5873 2.28695 15.3562 2.25697 15.1042L2.25 14.9877V9.01298C2.25 8.74888 2.35491 8.4956 2.54166 8.30885C2.7284 8.12211 2.98168 8.0172 3.24578 8.0172Z"
fill="currentColor"
/>
<path
d="M6.23312 8.0172C6.47702 8.01723 6.71243 8.10677 6.89469 8.26885C7.07696 8.43092 7.1934 8.65425 7.22193 8.89647L7.2289 9.01298V14.9877C7.22862 15.2415 7.13144 15.4856 6.95721 15.6701C6.78298 15.8547 6.54485 15.9658 6.29148 15.9806C6.03811 15.9955 5.78863 15.9131 5.594 15.7502C5.39937 15.5873 5.27429 15.3562 5.24431 15.1042L5.23734 14.9877V9.01298C5.23734 8.74888 5.34226 8.4956 5.529 8.30885C5.71575 8.12211 5.96903 8.0172 6.23312 8.0172Z"
fill="currentColor"
/>
</svg>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

@@ -0,0 +1,33 @@
import { Header3 } from "./primitives/Headers";
import { Paragraph } from "./primitives/Paragraph";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./primitives/Tooltip";
export function DefinitionTip({
content,
children,
title,
}: {
content: React.ReactNode;
children: React.ReactNode;
title: React.ReactNode;
}) {
return (
<TooltipProvider>
<Tooltip disableHoverableContent>
<TooltipTrigger>
<span className="underline decoration-slate-600 decoration-dashed underline-offset-4 transition hover:decoration-slate-500">
{children}
</span>
</TooltipTrigger>
<TooltipContent align="end" side="right" variant="dark" className="w-[16rem] min-w-[16rem]">
<Header3 className="mb-1">{title}</Header3>
{typeof content === "string" ? (
<Paragraph variant="small">{content}</Paragraph>
) : (
<div>{content}</div>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
+26 -24
View File
@@ -1,12 +1,15 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
import { DiscordIcon } from "@trigger.dev/companyicons";
import { ReactNode, useState } from "react";
import { FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
import { Button } from "./primitives/Buttons";
import { Fieldset } from "./primitives/Fieldset";
import { FormButtons } from "./primitives/FormButtons";
import { FormError } from "./primitives/FormError";
import { Header2 } from "./primitives/Headers";
import { InputGroup } from "./primitives/InputGroup";
import { Label } from "./primitives/Label";
import { Paragraph } from "./primitives/Paragraph";
@@ -20,8 +23,6 @@ import {
} from "./primitives/Select";
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "./primitives/Sheet";
import { TextArea } from "./primitives/TextArea";
import { DiscordIcon } from "@trigger.dev/companyicons";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
type FeedbackProps = {
button: ReactNode;
@@ -36,7 +37,8 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
const [form, { path, feedbackType, message }] = useForm({
id: "accept-invite",
lastSubmission,
// TODO: type this
lastSubmission: lastSubmission as any,
onValidate({ formData }) {
return parse(formData, { schema });
},
@@ -54,19 +56,16 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild={true}>{button}</SheetTrigger>
<SheetContent size="sm">
<SheetHeader className="justify-between">Help & feedback</SheetHeader>
<SheetBody>
<SheetContent>
<SheetHeader className="justify-between">How can we help?</SheetHeader>
<SheetBody className="flex h-full flex-col justify-between">
<DiscordBanner />
<Paragraph variant="small" className="mb-4 border-t border-slate-800 pt-3">
Or use this form to ask for help or give us feedback. We read every message and will get
back to you as soon as we can.
</Paragraph>
<hr className="mb-3" />
<Header2 className="mb-4">Send us a message</Header2>
<Form method="post" action="/resources/feedback" {...form.props}>
<Fieldset className="max-w-full">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<InputGroup className="max-w-full">
<Label>How can we help?</Label>
<SelectGroup>
<Select {...conform.input(feedbackType)} defaultValue={defaultValue}>
<SelectTrigger size="medium" width="full">
@@ -89,14 +88,19 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
<FormError id={message.errorId}>{message.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<FormButtons
className="w-full"
confirmButton={
<Button type="submit" variant="primary/medium">
Send
</Button>
}
/>
<div className="flex w-full items-center justify-between">
<Paragraph variant="small" className="w-full">
We read every message and respond quickly.
</Paragraph>
<FormButtons
className="m-0 w-max"
confirmButton={
<Button type="submit" variant="primary/medium">
Send
</Button>
}
/>
</div>
</Fieldset>
</Form>
</SheetBody>
@@ -108,7 +112,7 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
function DiscordBanner() {
return (
<a
href="https://discord.gg/nkqV9xBYWy"
href="https://trigger.dev/discord"
target="_blank"
className="group mb-4 flex w-full items-center justify-between rounded-md border border-slate-600 bg-gradient-to-br from-blue-400/30 to-indigo-400/50 p-4 transition hover:border-indigo-400"
>
@@ -119,13 +123,11 @@ function DiscordBanner() {
<br />
Discord community
</h2>
<Paragraph variant="small">
<Paragraph variant="small/bright">
Get help or answer questions from the Trigger.dev community.
</Paragraph>
</div>
<div className="h-full">
<ChevronRightIcon className="h-5 w-5 text-slate-400 transition group-hover:translate-x-1 group-hover:text-indigo-400" />
</div>
<ChevronRightIcon className="h-5 w-5 text-slate-400 transition group-hover:translate-x-1 group-hover:text-indigo-400" />
</a>
);
}
@@ -1,24 +1,18 @@
import { Form } from "@remix-run/react";
import { Paragraph } from "./primitives/Paragraph";
import { Button } from "./primitives/Buttons";
import { UserMinusIcon } from "@heroicons/react/20/solid";
export function ImpersonationBanner({ impersonationId }: { impersonationId: string }) {
export function ImpersonationBanner() {
return (
<div className="grid h-8 grid-cols-3 items-center bg-gradient-to-r from-amber-500 via-amber-400 to-amber-500 px-4 text-center">
<span></span>
<Paragraph variant="small">
You are impersonating <span className="font-semibold">{impersonationId}</span>
</Paragraph>
<Form
action="/resources/impersonation"
method="delete"
reloadDocument
className="justify-self-end"
>
<div className="w-full">
<Form action="/resources/impersonation" method="delete" reloadDocument className="w-full">
<Button
type="submit"
className="!text-slate-700 transition hover:!text-slate-900"
variant="secondary/small"
variant="small-menu-item"
LeadingIcon={UserMinusIcon}
fullWidth
textAlignLeft
>
Stop impersonating
</Button>
+22 -1
View File
@@ -16,19 +16,23 @@ export type JobEnvironment = {
lastRun?: Date;
version: string;
enabled: boolean;
concurrencyLimit?: number | null;
concurrencyLimitGroup?: { name: string; concurrencyLimit: number } | null;
};
type JobStatusTableProps = {
environments: JobEnvironment[];
displayStyle?: "short" | "long";
};
export function JobStatusTable({ environments }: JobStatusTableProps) {
export function JobStatusTable({ environments, displayStyle = "short" }: JobStatusTableProps) {
return (
<Table fullWidth>
<TableHeader>
<TableRow>
<TableHeaderCell>Env</TableHeaderCell>
<TableHeaderCell>Last Run</TableHeaderCell>
{displayStyle === "long" && <TableHeaderCell>Concurrency</TableHeaderCell>}
<TableHeaderCell alignment="right">Version</TableHeaderCell>
<TableHeaderCell alignment="right">Status</TableHeaderCell>
</TableRow>
@@ -42,6 +46,23 @@ export function JobStatusTable({ environments }: JobStatusTableProps) {
<TableCell>
{environment.lastRun ? <DateTime date={environment.lastRun} /> : "Never Run"}
</TableCell>
{displayStyle === "long" && (
<TableCell>
{environment.concurrencyLimitGroup ? (
<span className="flex items-center gap-1">
<span>{environment.concurrencyLimitGroup.name}</span>
<span className="text-gray-400">
({environment.concurrencyLimitGroup.concurrencyLimit})
</span>
</span>
) : typeof environment.concurrencyLimit === "number" ? (
<span className="text-gray-400">{environment.concurrencyLimit}</span>
) : (
<span className="text-gray-400">Not specified</span>
)}
</TableCell>
)}
<TableCell alignment="right">{environment.version}</TableCell>
<TableCell alignment="right">
<ActiveBadge active={environment.enabled} />
@@ -0,0 +1,341 @@
import {
BoltIcon,
CloudIcon,
CodeBracketIcon,
CodeBracketSquareIcon,
HeartIcon,
ServerStackIcon,
} from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
import { cn } from "~/utils/cn";
import { LogoType } from "./LogoType";
import { LinkButton } from "./primitives/Buttons";
import { Header3 } from "./primitives/Headers";
import { Icon } from "./primitives/Icon";
import { Paragraph } from "./primitives/Paragraph";
import { TextLink } from "./primitives/TextLink";
import { LoginTooltip } from "./primitives/Tooltip";
interface QuoteType {
quote: string;
person: string;
}
const quotes: QuoteType[] = [
{
quote: "Trigger.dev is redefining background jobs for modern developers.",
person: "Paul Copplestone, Supabase",
},
{
quote:
"Trigger.dev is a great way to automate email campaigns with Resend, and we've heard nothing but good things from our mutual customers.",
person: "Zeno Rocha, Resend",
},
{
quote: "We love Trigger.dev and its had a big impact in dev iteration velocity already.",
person: "André Neves, ZBD",
},
{
quote:
"Weve been looking for a product like Trigger.dev for a really long time - automation that's simple and developer-focused.",
person: "Han Wang, Mintlify",
},
];
const layout = "group grid place-items-center text-center overflow-hidden";
const gridCell = "hover:bg-midnight-850 rounded-lg transition bg-midnight-850/40";
const opacity = "opacity-10 group-hover:opacity-100 transition group-hover:scale-105";
const logos = "h-[60%] w-[60%] transition grayscale group-hover:grayscale-0";
const features = "h-[60%] w-[60%] text-gray-500 grayscale transition group-hover:grayscale-0";
const wide = "col-span-2";
const wider = "col-span-3 row-span-2";
const mediumSquare = "col-span-2 row-span-2";
const hidden = "hidden xl:grid";
export function LoginPageLayout({ children }: { children: React.ReactNode }) {
const [randomQuote, setRandomQuote] = useState<QuoteType | null>(null);
useEffect(() => {
const randomIndex = Math.floor(Math.random() * quotes.length);
setRandomQuote(quotes[randomIndex]);
}, []);
return (
<main className="grid h-full grid-cols-12">
<div className="border-midnight-750 z-10 col-span-12 border-r bg-midnight-850 md:col-span-6">
<div className="flex h-full flex-col items-center justify-between p-6">
<div className="flex w-full items-center justify-between">
<a href="https://trigger.dev">
<LogoType className="w-36" />
</a>
<LinkButton
to="https://trigger.dev/docs"
variant={"secondary/small"}
LeadingIcon="docs"
>
Documentation
</LinkButton>
</div>
<div className="flex h-full max-w-sm items-center justify-center">{children}</div>
<Paragraph variant="extra-small" className="text-center">
Having login issues? <TextLink href="mailto:help@trigger.dev">Email us</TextLink> or{" "}
<TextLink href="https://trigger.dev/discord">ask us in Discord</TextLink>
</Paragraph>
</div>
</div>
<div className="hidden grid-cols-3 grid-rows-6 gap-4 p-4 md:col-span-6 md:grid xl:grid-cols-5">
<LoginTooltip side="bottom" content={<ServerlessTooltipContent />}>
<div className={cn(layout, gridCell, mediumSquare)}>
<ServerStackIcon className={cn(opacity, features, "group-hover:text-green-500")} />
</div>
</LoginTooltip>
<LoginTooltip side="bottom" content={<SlackTooltipContent />}>
<div className={cn(layout, gridCell)}>
<Icon icon="slack" className={cn(logos, opacity)} />
</div>
</LoginTooltip>
<LoginTooltip side="left" content={<TriggerTooltipContent />}>
<div className={cn(layout, gridCell, mediumSquare, hidden)}>
<BoltIcon className={cn(opacity, features, "group-hover:text-yellow-500")} />
</div>
</LoginTooltip>
<LoginTooltip side="bottom" content={<StripeTooltipContent />} className="max-w-[15rem]">
<div className={cn("", layout, gridCell)}>
<Icon icon="stripe" className={cn(logos, opacity)} />
</div>
</LoginTooltip>
<LoginTooltip side="top" content={<QuoteTooltipContent />}>
<div className={cn(layout, gridCell, wider)}>
<div className="px-4">
<Header3 className="relative text-2xl font-normal leading-8 text-gray-600 transition before:relative before:right-1 before:top-0 before:text-4xl before:text-slate-600 before:opacity-20 before:content-['❝'] group-hover:text-slate-500 group-hover:before:opacity-30 lg-height:text-xl md-height:text-lg">
{randomQuote?.quote}
</Header3>
<Paragraph
variant="small"
className="mt-4 text-gray-700 transition group-hover:text-slate-600"
>
{randomQuote?.person}
</Paragraph>
</div>
</div>
</LoginTooltip>
<LoginTooltip side="left" content={<OpenaiTooltipContent />}>
<div className={cn("", layout, gridCell, hidden)}>
<Icon icon="openai" className={cn(logos, opacity)} />
</div>
</LoginTooltip>
<LoginTooltip side="left" content={<SendgridTooltipContent />}>
<div className={cn(layout, gridCell, hidden)}>
<Icon icon="sendgrid" className={cn(logos, opacity)} />
</div>
</LoginTooltip>
<LoginTooltip side="left" content={<ReactHooksTooltipContent />}>
<div className={cn(layout, gridCell, mediumSquare, hidden)}>
<Icon icon="react" className={cn(opacity, features, "group-hover:text-green-500")} />
</div>
</LoginTooltip>
<LoginTooltip side="right" content={<AirtableTooltipContent />}>
<div className={cn("", layout, gridCell)}>
<Icon icon="airtable" className={cn(logos, opacity)} />
</div>
</LoginTooltip>
<LoginTooltip side="top" content={<InYourCodebaseTooltipContent />}>
<div className={cn(layout, gridCell, mediumSquare)}>
<CodeBracketSquareIcon className={cn(opacity, features, "group-hover:text-rose-500")} />
</div>
</LoginTooltip>
<LoginTooltip side="right" content={<SupabaseTooltipContent />}>
<div className={cn(layout, gridCell)}>
<Icon icon="supabase" className={cn(logos, opacity)} />
</div>
</LoginTooltip>
<LoginTooltip side="left" content={<CloudTooltipContent />}>
<div className={cn(layout, gridCell, wide, hidden)}>
<CloudIcon className={cn(opacity, features, "h-20 w-20 group-hover:text-blue-600")} />
</div>
</LoginTooltip>
</div>
</main>
);
}
function SlackTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<Icon icon="slack" className="h-5 w-5" />
<Paragraph variant="base/bright" className="font-semibold">
Slack Integration
</Paragraph>
</div>
<Paragraph variant="base">Post messages to your team when your Job is triggered.</Paragraph>
</>
);
}
function StripeTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<Icon icon="stripe" className="h-5 w-5" />
<Paragraph variant="base/bright" className="font-semibold">
Stripe Integration
</Paragraph>
</div>
<Paragraph variant="base">Trigger payments, emails, subscription upgrades</Paragraph>
</>
);
}
function SupabaseTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<Icon icon="supabase" className="h-5 w-5" />
<Paragraph variant="base/bright" className="font-semibold">
Supabase Integration
</Paragraph>
</div>
<Paragraph variant="base">React to changes in your database.</Paragraph>
</>
);
}
function SendgridTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<Icon icon="sendgrid" className="h-5 w-5" />
<Paragraph variant="base/bright" className="font-semibold">
SendGrid Integration
</Paragraph>
</div>
<Paragraph variant="base">
Create a drip campaign, trigger an onboarding sequence and more
</Paragraph>
</>
);
}
function AirtableTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<Icon icon="airtable" className="h-5 w-5" />
<Paragraph variant="base/bright" className="font-semibold">
Airtable Integration
</Paragraph>
</div>
<Paragraph variant="base">
Update your Airtable records when you make a Stripe sale, receive a new Typeform response
and more
</Paragraph>
</>
);
}
function OpenaiTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<Icon icon="openai" className="h-5 w-5" />
<Paragraph variant="base/bright" className="font-semibold">
OpenAI Integration
</Paragraph>
</div>
<Paragraph variant="base">Generate text, images, code and more with OpenAI's API.</Paragraph>
</>
);
}
function TriggerTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<BoltIcon className="h-5 w-5 text-yellow-500" />
<Paragraph variant="base/bright" className="font-semibold">
Triggering your Job
</Paragraph>
</div>
<Paragraph variant="base">
Trigger your Jobs with a webhook, on a recurring schedule, or from your own custom events.
</Paragraph>
</>
);
}
function QuoteTooltipContent() {
return (
<>
<div className="flex items-center gap-x-1.5">
<HeartIcon className="h-5 w-5 text-rose-500" />
<Paragraph variant="base/bright" className="font-semibold">
Loved by developers
</Paragraph>
</div>
</>
);
}
function InYourCodebaseTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<CodeBracketSquareIcon className="h-5 w-5 text-rose-500" />
<Paragraph variant="base/bright" className="font-semibold">
In your codebase
</Paragraph>
</div>
<Paragraph variant="base">
Create background jobs where they belong: in your codebase.
</Paragraph>
</>
);
}
function CloudTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<CloudIcon className="h-5 w-5 text-blue-600" />
<Paragraph variant="base/bright" className="font-semibold">
Zero infrastructure
</Paragraph>
</div>
<Paragraph variant="base">
Use our SDK to write Jobs in your codebase and deploy as you normally do.
</Paragraph>
</>
);
}
function ServerlessTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<ServerStackIcon className="h-5 w-5 text-green-500" />
<Paragraph variant="base/bright" className="font-semibold">
Full serverless support
</Paragraph>
</div>
<Paragraph variant="base">
Run long-running background jobs without worrying about timeouts.
</Paragraph>
</>
);
}
function ReactHooksTooltipContent() {
return (
<>
<div className="mb-2 flex items-center gap-x-1.5">
<Icon icon="react" className="h-5 w-5" />
<Paragraph variant="base/bright" className="font-semibold">
Show Job progress in your UI
</Paragraph>
</div>
<Paragraph variant="base">
Use our React hooks to display a real-time status to your users.
</Paragraph>
</>
);
}
+190
View File
@@ -0,0 +1,190 @@
export function LogoType({ className }: { className?: string }) {
return (
<svg viewBox="0 0 751 130" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
d="M195.022 16.2676H135.445H137.799V32.5096H157.858V102.4H174.84V32.5096H195.022V16.2676Z"
fill="url(#paint0_linear_228_1439)"
/>
<path
d="M211.265 51.4587V40.8767H195.391V102.4H211.265V72.9917C211.265 60.0719 221.725 56.3805 229.97 57.3648V39.6463C222.218 39.6463 214.465 43.0916 211.265 51.4587Z"
fill="url(#paint1_linear_228_1439)"
/>
<path
d="M246.954 33.494C252.368 33.494 256.799 29.0644 256.799 23.7734C256.799 18.4824 252.368 13.9297 246.954 13.9297C241.662 13.9297 237.232 18.4824 237.232 23.7734C237.232 29.0644 241.662 33.494 246.954 33.494ZM239.078 102.4H254.953V40.8767H239.078V102.4Z"
fill="url(#paint2_linear_228_1439)"
/>
<path
d="M315.253 40.8768V48.5056C310.946 42.7224 304.301 39.1542 295.563 39.1542C278.089 39.1542 264.921 53.4275 264.921 70.6539C264.921 88.0033 278.089 102.154 295.563 102.154C304.301 102.154 310.946 98.5853 315.253 92.8021V99.4466C315.253 109.167 309.1 114.581 299.132 114.581C289.656 114.581 285.596 110.767 283.011 105.968L269.475 113.72C274.889 123.687 285.472 128.731 298.64 128.731C314.884 128.731 330.758 119.626 330.758 99.4466V40.8768H315.253ZM298.025 87.5112C288.057 87.5112 280.796 80.4975 280.796 70.6539C280.796 60.9332 288.057 53.9196 298.025 53.9196C307.992 53.9196 315.253 60.9332 315.253 70.6539C315.253 80.4975 307.992 87.5112 298.025 87.5112Z"
fill="url(#paint3_linear_228_1439)"
/>
<path
d="M390.936 40.8768V48.5056C386.629 42.7224 379.983 39.1542 371.246 39.1542C353.772 39.1542 340.604 53.4275 340.604 70.6539C340.604 88.0033 353.772 102.154 371.246 102.154C379.983 102.154 386.629 98.5853 390.936 92.8021V99.4466C390.936 109.167 384.783 114.581 374.815 114.581C365.339 114.581 361.278 110.767 358.694 105.968L345.157 113.72C350.572 123.687 361.155 128.731 374.322 128.731C390.566 128.731 406.441 119.626 406.441 99.4466V40.8768H390.936ZM373.707 87.5112C363.739 87.5112 356.479 80.4975 356.479 70.6539C356.479 60.9332 363.739 53.9196 373.707 53.9196C383.675 53.9196 390.936 60.9332 390.936 70.6539C390.936 80.4975 383.675 87.5112 373.707 87.5112Z"
fill="url(#paint4_linear_228_1439)"
/>
<path
d="M432.9 78.1597H479.293C479.663 76.0679 479.909 73.9761 479.909 71.6383C479.909 53.5505 466.987 39.1542 448.775 39.1542C429.454 39.1542 416.287 53.3044 416.287 71.6383C416.287 89.9721 429.331 104.122 450.005 104.122C461.819 104.122 471.048 99.3236 476.832 90.9564L464.034 83.5737C461.327 87.142 456.404 89.726 450.251 89.726C441.883 89.726 435.115 86.2807 432.9 78.1597ZM432.654 65.8551C434.5 57.9802 440.284 53.4274 448.775 53.4274C455.42 53.4274 462.065 56.9958 464.034 65.8551H432.654Z"
fill="url(#paint5_linear_228_1439)"
/>
<path
d="M505.199 51.4587V40.8767H489.324V102.4H505.199V72.9917C505.199 60.0719 515.659 56.3805 523.904 57.3648V39.6463C516.151 39.6463 508.398 43.0916 505.199 51.4587Z"
fill="url(#paint6_linear_228_1439)"
/>
<path
d="M529.934 103.999C535.717 103.999 540.394 99.3235 540.394 93.5404C540.394 87.7572 535.717 83.0815 529.934 83.0815C524.15 83.0815 519.473 87.7572 519.473 93.5404C519.473 99.3235 524.15 103.999 529.934 103.999Z"
fill="url(#paint7_linear_228_1439)"
/>
<path
d="M596.632 16.2676V48.1364C592.202 42.4763 585.679 39.1541 576.696 39.1541C560.206 39.1541 546.67 53.3044 546.67 71.6382C546.67 89.972 560.206 104.122 576.696 104.122C585.679 104.122 592.202 100.8 596.632 95.1399V102.4H612.506V16.2676L596.632 16.2676ZM579.65 88.9876C569.805 88.9876 562.544 81.9741 562.544 71.6382C562.544 61.3024 569.805 54.2887 579.65 54.2887C589.371 54.2887 596.632 61.3024 596.632 71.6382C596.632 81.9741 589.371 88.9876 579.65 88.9876Z"
fill="url(#paint8_linear_228_1439)"
/>
<path
d="M637.98 78.1597H684.373C684.742 76.0679 684.989 73.9761 684.989 71.6383C684.989 53.5505 672.067 39.1542 653.855 39.1542C634.534 39.1542 621.367 53.3044 621.367 71.6383C621.367 89.9721 634.411 104.122 655.085 104.122C666.899 104.122 676.128 99.3236 681.912 90.9564L669.114 83.5737C666.407 87.142 661.484 89.726 655.331 89.726C646.963 89.726 640.195 86.2807 637.98 78.1597ZM637.734 65.8551C639.58 57.9802 645.363 53.4274 653.855 53.4274C660.5 53.4274 667.145 56.9958 669.114 65.8551H637.734Z"
fill="url(#paint9_linear_228_1439)"
/>
<path
d="M732.859 40.8768L717.846 83.9428L702.955 40.8768H685.481L708.862 102.4H726.952L750.333 40.8768H732.859Z"
fill="url(#paint10_linear_228_1439)"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M35.664 42.3949L59.4114 1.26865L118.264 103.194H0.558823L24.3062 62.0665L41.1046 71.7643L34.157 83.7971H84.6657L59.4114 40.0612L52.4637 52.094L35.664 42.3949Z"
fill="url(#paint11_linear_228_1439)"
/>
<defs>
<linearGradient
id="paint0_linear_228_1439"
x1="329.674"
y1="150.079"
x2="329.674"
y2="13.9297"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
<linearGradient
id="paint1_linear_228_1439"
x1="329.674"
y1="150.079"
x2="329.674"
y2="13.9297"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
<linearGradient
id="paint2_linear_228_1439"
x1="329.674"
y1="150.079"
x2="329.674"
y2="13.9297"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
<linearGradient
id="paint3_linear_228_1439"
x1="329.674"
y1="150.079"
x2="329.674"
y2="13.9297"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
<linearGradient
id="paint4_linear_228_1439"
x1="329.674"
y1="150.079"
x2="329.674"
y2="13.9297"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
<linearGradient
id="paint5_linear_228_1439"
x1="329.674"
y1="150.079"
x2="329.674"
y2="13.9297"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
<linearGradient
id="paint6_linear_228_1439"
x1="329.674"
y1="150.079"
x2="329.674"
y2="13.9297"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
<linearGradient
id="paint7_linear_228_1439"
x1="634.903"
y1="139.717"
x2="651.436"
y2="25.9719"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2563EB" />
<stop offset="1" stopColor="#A855F7" />
</linearGradient>
<linearGradient
id="paint8_linear_228_1439"
x1="634.903"
y1="139.717"
x2="651.436"
y2="25.9719"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2563EB" />
<stop offset="1" stopColor="#A855F7" />
</linearGradient>
<linearGradient
id="paint9_linear_228_1439"
x1="634.903"
y1="139.717"
x2="651.436"
y2="25.9719"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2563EB" />
<stop offset="1" stopColor="#A855F7" />
</linearGradient>
<linearGradient
id="paint10_linear_228_1439"
x1="634.903"
y1="139.717"
x2="651.436"
y2="25.9719"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#2563EB" />
<stop offset="1" stopColor="#A855F7" />
</linearGradient>
<linearGradient
id="paint11_linear_228_1439"
x1="95.8593"
y1="103.194"
x2="94.7607"
y2="31.2381"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41FF54" />
<stop offset="1" stopColor="#E7FF52" />
</linearGradient>
</defs>
</svg>
);
}
@@ -1,12 +0,0 @@
import gradientBackground from "~/assets/images/gradient-background.png";
export function PageGradient({ children }: { children: React.ReactNode }) {
return (
<div
className="-mt-4 h-full w-full bg-cover bg-no-repeat pt-20"
style={{ backgroundImage: `url("${gradientBackground}")` }}
>
{children}
</div>
);
}
+39 -27
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,34 +56,67 @@ 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 the default you can specify the port by adding{" "}
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
@@ -0,0 +1,123 @@
import {
Label,
Line,
LineChart,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { Paragraph } from "../primitives/Paragraph";
const tooltipStyle = {
display: "flex",
alignItems: "center",
gap: "0.5rem",
borderRadius: "0.25rem",
border: "1px solid #1A2434",
backgroundColor: "#0B1018",
padding: "0.3rem 0.5rem",
fontSize: "0.75rem",
color: "#E2E8F0",
};
type DataItem = { date: string; maxConcurrentRuns: number };
const dateFormatter = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
});
export function ConcurrentRunsChart({
concurrentRunsLimit,
data,
hasConcurrencyData,
}: {
concurrentRunsLimit?: number;
data: DataItem[];
hasConcurrencyData: boolean;
}) {
return (
<div className="relative">
{!hasConcurrencyData && (
<Paragraph className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
No concurrent Runs to show
</Paragraph>
)}
<ResponsiveContainer width="100%" height="100%" className="relative min-h-[20rem]">
<LineChart
data={data}
margin={{
top: 20,
right: 0,
left: 0,
bottom: 10,
}}
className="-ml-8"
>
<XAxis
stroke="#94A3B8"
fontSize={12}
tickLine={false}
axisLine={false}
dataKey={(item: DataItem) => {
if (!item.date) return "";
const date = new Date(item.date);
if (date.getDate() === 1) {
return dateFormatter.format(date);
}
return `${date.getDate()}`;
}}
className="text-xs"
>
<Label value="Last 30 days" offset={-8} position="insideBottom" fill="#94A3B8" />
</XAxis>
<YAxis
stroke="#94A3B8"
fontSize={12}
tickLine={false}
axisLine={false}
allowDecimals={false}
/>
<Tooltip
cursor={{ fill: "rgba(255,255,255,0.05)" }}
contentStyle={tooltipStyle}
labelFormatter={(value, data) => {
const dateString = data.at(0)?.payload.date;
if (!dateString) {
return "";
}
return dateFormatter.format(new Date(dateString));
}}
/>
{concurrentRunsLimit && (
<ReferenceLine
y={concurrentRunsLimit}
stroke="#F43F5E"
strokeWidth={1}
strokeDasharray={5}
ifOverflow="extendDomain"
className="text-xs"
>
<Label
value="Concurrent Runs limit"
offset={8}
position="insideTopLeft"
fill="#F43F5E"
/>
</ReferenceLine>
)}
<Line
dataKey="maxConcurrentRuns"
name="Concurrent runs"
stroke="#16A34A"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}
@@ -0,0 +1,89 @@
import { Label, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { Paragraph } from "../primitives/Paragraph";
const tooltipStyle = {
display: "flex",
alignItems: "center",
gap: "0.5rem",
borderRadius: "0.25rem",
border: "1px solid #1A2434",
backgroundColor: "#0B1018",
padding: "0.3rem 0.5rem",
fontSize: "0.75rem",
color: "#E2E8F0",
};
type DataItem = { date: string; runs: number };
const dateFormatter = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
});
export function DailyRunsChart({
data,
hasDailyRunsData,
}: {
data: DataItem[];
hasDailyRunsData: boolean;
}) {
return (
<div className="relative">
{!hasDailyRunsData && (
<Paragraph className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
No daily Runs to show
</Paragraph>
)}
<ResponsiveContainer width="100%" height="100%" className="relative min-h-[20rem]">
<LineChart
data={data}
margin={{
top: 20,
right: 0,
left: 0,
bottom: 10,
}}
className="-ml-8"
>
<XAxis
stroke="#94A3B8"
fontSize={12}
tickLine={false}
axisLine={false}
dataKey={(item: DataItem) => {
if (!item.date) return "";
const date = new Date(item.date);
if (date.getDate() === 1) {
return dateFormatter.format(date);
}
return `${date.getDate()}`;
}}
className="text-xs"
>
<Label value="Last 30 days" offset={-8} position="insideBottom" fill="#94A3B8" />
</XAxis>
<YAxis
stroke="#94A3B8"
fontSize={12}
tickLine={false}
axisLine={false}
allowDecimals={false}
/>
<Tooltip
cursor={{ fill: "rgba(255,255,255,0.05)" }}
contentStyle={tooltipStyle}
labelFormatter={(value, data) => {
const dateString = data.at(0)?.payload.date;
if (!dateString) {
return "";
}
return dateFormatter.format(new Date(dateString));
}}
/>
<Line dataKey="runs" name="Runs" stroke="#16A34A" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
);
}
@@ -0,0 +1,47 @@
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
import { motion, useMotionValue, useTransform } from "framer-motion";
import { Paragraph } from "../primitives/Paragraph";
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
export function FreePlanUsage({ to, percentage }: { to: string; percentage: number }) {
const cappedPercentage = Math.min(percentage, 1);
const widthProgress = useMotionValue(cappedPercentage * 100);
const color = useTransform(
widthProgress,
[0, 74, 75, 95, 100],
["#22C55E", "#22C55E", "#F59E0B", "#F43F5E", "#F43F5E"]
);
const hasHitLimit = cappedPercentage >= 1;
return (
<div
className={cn(
"rounded border border-slate-900 bg-[#101722] p-2.5",
hasHitLimit && "border-rose-800/60"
)}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1">
<ArrowUpCircleIcon className="h-5 w-5 text-dimmed" />
<Paragraph className="text-2sm text-bright">Free Plan</Paragraph>
</div>
<Link to={to} className="text-2sm text-indigo-500">
Learn more
</Link>
</div>
<div className="relative mt-3 h-1 rounded-full bg-[#0B1018]">
<motion.div
initial={{ width: 0 }}
animate={{ width: cappedPercentage * 100 + "%" }}
style={{
backgroundColor: color,
}}
transition={{ duration: 1, type: "spring" }}
className={cn("absolute left-0 top-0 h-full rounded-full")}
/>
</div>
</div>
);
}
@@ -0,0 +1,259 @@
import * as Slider from "@radix-ui/react-slider";
import { Plans, estimate } from "@trigger.dev/billing";
import { useCallback, useState } from "react";
import { DefinitionTip } from "../DefinitionTooltip";
import { Header2 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
import { formatCurrency, formatNumberCompact } from "~/utils/numberFormatter";
import { cn } from "~/utils/cn";
export function PricingCalculator({ plans }: { plans: Plans }) {
const [selectedConcurrencyIndex, setSelectedConcurrencyIndex] = useState(0);
const concurrentRunTiers = [
{ code: "free", upto: plans.free.concurrentRuns?.freeAllowance! },
...(plans.paid.concurrentRuns?.pricing?.tiers ?? []),
];
const [runs, setRuns] = useState(0);
const runBrackets = [
...(plans.paid.runs?.pricing?.brackets.map((b, index, arr) => ({
unitCost: b.unitCost,
from: index === 0 ? 0 : arr[index - 1].upto! + 1,
upto: b.upto ?? arr[index - 1].upto! * 10,
})) ?? []),
];
const result = estimate({
usage: { runs, concurrent_runs: concurrentRunTiers[selectedConcurrencyIndex].upto - 1 },
plans: [plans.free, plans.paid],
});
return (
<div className="flex w-full flex-col gap-4">
<ConcurrentRunsSlider
options={concurrentRunTiers}
selectedIndex={selectedConcurrencyIndex}
setSelectedIndex={setSelectedConcurrencyIndex}
cost={result?.cost.concurrentRunCost ?? 0}
/>
<RunsSlider
brackets={runBrackets}
runs={runs}
setRuns={setRuns}
cost={result?.cost.runsCost ?? 0}
/>
<GrandTotal cost={result?.cost.total ?? 0} />
</div>
);
}
function ConcurrentRunsSlider({
options,
selectedIndex,
setSelectedIndex,
cost,
}: {
options: {
code: string;
upto: number;
}[];
selectedIndex: number;
setSelectedIndex: (index: number) => void;
cost: number;
}) {
const selectedOption = options[selectedIndex];
return (
<div>
<div className="flex">
<div className="flex w-full flex-col">
<div className="flex items-center justify-between">
<Header2>
<DefinitionTip
content="The number of runs that can be executed at the same time. Get in touch if you need more than 100."
title="Concurrent runs"
>
Concurrent runs
</DefinitionTip>
</Header2>
<Header2>Up to {selectedOption.upto}</Header2>
</div>
<Slider.Root
className="relative mb-2 mt-4 flex h-5 w-full touch-none select-none items-center"
value={[selectedIndex]}
onValueChange={(value) => setSelectedIndex(value[0])}
max={options.length - 1}
step={1}
>
<Slider.Track className="relative h-[8px] grow rounded-full bg-slate-850">
<Slider.Range className="absolute h-full rounded-full bg-indigo-500" />
</Slider.Track>
<Slider.Thumb
className="block h-5 w-5 rounded-full border-4 border-indigo-500 bg-slate-850 shadow-[0_1px_3px_4px_rgb(0_0_0_/_0.2),_0_1px_2px_-1px_rgb(0_0_0_/_0.1)] transition hover:border-indigo-400 hover:bg-slate-800 focus:shadow-[0_1px_3px_4px_rgb(0_0_0_/_0.2),_0_1px_2px_-1px_rgb(0_0_0_/_0.1)] focus:outline-none"
aria-label="Concurrent runs slider"
/>
</Slider.Root>
<div className="ml-1.5 flex w-[99.85%] items-center justify-between">
{options.map((tier, i) => {
return (
<Paragraph variant="extra-small" className="text-slate-600" key={i}>
{tier.upto}
</Paragraph>
);
})}
</div>
</div>
<div className="flex h-full items-start">
<span className="ml-6 text-dimmed">=</span>
<Header2 className="min-w-[8ch] text-right text-dimmed">
{formatCurrency(cost, true)}
</Header2>
</div>
</div>
<hr className="mt-6 border-border" />
</div>
);
}
const runIncrements = 10_000;
function RunsSlider({
brackets,
runs,
setRuns,
cost,
}: {
brackets: {
from: number;
upto: number;
unitCost: number;
}[];
runs: number;
setRuns: (value: number) => void;
cost: number;
}) {
const [value, setValue] = useState(0);
const updateRuns = useCallback((value: number) => {
setValue(value);
const r = calculateRuns(value / runIncrements, brackets);
setRuns(r);
}, []);
return (
<div>
<div className="flex">
<div className="flex w-full flex-col">
<div className="flex items-center justify-between">
<Header2>
<DefinitionTip content="A single execution of a Job." title="Runs">
Runs
</DefinitionTip>
</Header2>
<Header2>{formatNumberCompact(runs)}</Header2>
</div>
<Slider.Root
className="relative mb-2 mt-4 flex h-5 w-full touch-none select-none items-center"
value={[value]}
onValueChange={(value) => updateRuns(value[0])}
max={runIncrements}
step={1}
>
<Slider.Track className="relative h-[8px] grow rounded-full bg-slate-850">
<Slider.Range className="absolute h-full rounded-full bg-indigo-500" />
</Slider.Track>
<Slider.Thumb
className="block h-5 w-5 rounded-full border-4 border-indigo-500 bg-slate-850 shadow-[0_1px_3px_4px_rgb(0_0_0_/_0.2),_0_1px_2px_-1px_rgb(0_0_0_/_0.1)] transition hover:border-indigo-400 hover:bg-slate-800 focus:shadow-[0_1px_3px_4px_rgb(0_0_0_/_0.2),_0_1px_2px_-1px_rgb(0_0_0_/_0.1)] focus:outline-none"
aria-label="Concurrent runs slider"
/>
</Slider.Root>
<div className="relative w-full">
{brackets.map((bracket, i, arr) => {
const percentagePerBracket = 1 / arr.length;
return (
<SliderMarker
key={i}
percentage={(i / (arr.length - 1)) * percentagePerBracket * (arr.length - 1)}
alignment={i === 0 ? "left" : "center"}
text={formatNumberCompact(bracket.from)}
/>
);
})}
<SliderMarker
percentage={1}
alignment={"right"}
text={formatNumberCompact(brackets[brackets.length - 1].upto)}
/>
</div>
</div>
<div className="flex h-full items-start">
<span className="ml-6 text-dimmed">=</span>
<Header2 className="min-w-[8ch] text-right text-dimmed">
{formatCurrency(cost, true)}
</Header2>
</div>
</div>
<hr className="mt-6 border-border" />
</div>
);
}
function calculateRuns(percentage: number, brackets: { from: number; upto: number }[]) {
//first we find which bucket we're in
const buckets = brackets.length;
const bucket = Math.min(Math.floor(percentage * buckets), brackets.length - 1);
const percentagePerBucket = 1 / buckets;
//relevant bracket
let bracket = brackets[bucket];
const from = bracket.from;
const upto = bracket.upto;
//how far as we into the bracket
const percentageIntoBracket = (percentage - bucket * percentagePerBucket) / percentagePerBucket;
//calculate the runs
const runs = Math.floor(from + (upto - from) * percentageIntoBracket);
return runs;
}
function GrandTotal({ cost }: { cost: number }) {
return (
<div className="flex justify-between">
<Header2>Total monthly estimate</Header2>
<Header2>{formatCurrency(cost, true)}</Header2>
</div>
);
}
function SliderMarker({
percentage,
alignment,
text,
}: {
percentage: number;
alignment: "left" | "center" | "right";
text: string;
}) {
return (
<div
className="absolute top-0 h-4"
style={{
left: `${percentage * 100}%`,
}}
>
<div
className={cn(
"absolute flex items-center",
alignment === "left"
? "left-0 justify-start"
: alignment === "center"
? "-translate-x-1/2 justify-center"
: "justify-middle right-0"
)}
>
<Paragraph variant="extra-small" className="text-slate-600">
{text}
</Paragraph>
</div>
</div>
);
}
@@ -0,0 +1,519 @@
import { useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/solid";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { ActiveSubscription, Plan, Plans, SetPlanBodySchema } from "@trigger.dev/billing";
import { useState } from "react";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { cn } from "~/utils/cn";
import { formatNumberCompact } from "~/utils/numberFormatter";
import { DefinitionTip } from "../DefinitionTooltip";
import { Feedback } from "../Feedback";
import { Button, LinkButton } from "../primitives/Buttons";
import SegmentedControl from "../primitives/SegmentedControl";
import { RunsVolumeDiscountTable } from "./RunsVolumeDiscountTable";
import { Spinner } from "../primitives/Spinner";
const pricingDefinitions = {
concurrentRuns: {
title: "Concurrent runs",
content: "The number of runs that can be executed at the same time.",
},
jobRuns: {
title: "Job runs",
content: "A single execution of a job.",
},
jobs: {
title: "Jobs",
content: "A durable function that can be executed on a schedule or in response to an event.",
},
tasks: {
title: "Tasks",
content: "The individual building blocks of a job run.",
},
events: {
title: "Events",
content: "Events trigger jobs to start running.",
},
integrations: {
title: "Integrations",
content: "Easily subscribe to webhooks and perform actions using APIs.",
},
};
export function PricingTiers({
organizationSlug,
plans,
className,
showActionText = true,
freeButtonPath,
}: {
organizationSlug: string;
plans: Plans;
className?: string;
showActionText?: boolean;
freeButtonPath?: string;
}) {
const currentPlan = useCurrentPlan();
//if they've canceled, we set the subscription to undefined so they can re-upgrade
let currentSubscription = currentPlan?.subscription;
if (currentPlan?.subscription?.canceledAt) {
currentSubscription = undefined;
}
return (
<div
className={cn(
"flex min-w-full flex-col items-start justify-center gap-4 md:flex-row",
className
)}
>
<TierFree
plan={plans.free}
currentSubscription={currentSubscription}
organizationSlug={organizationSlug}
showActionText={showActionText}
buttonPath={freeButtonPath}
/>
<TierPro
plan={plans.paid}
currentSubscription={currentSubscription}
organizationSlug={organizationSlug}
showActionText={showActionText}
/>
<TierEnterprise />
</div>
);
}
export function TierFree({
plan,
organizationSlug,
showActionText,
currentSubscription,
buttonPath,
}: {
plan: Plan;
organizationSlug: string;
showActionText: boolean;
currentSubscription?: ActiveSubscription;
buttonPath?: string;
}) {
const lastSubmission = useActionData();
const [form] = useForm({
id: "subscribe",
// TODO: type this
lastSubmission: lastSubmission as any,
onValidate({ formData }) {
return parse(formData, { schema: SetPlanBodySchema });
},
});
const navigation = useNavigation();
const isLoading =
(navigation.state === "submitting" || navigation.state === "loading") &&
navigation.formData?.get("type") === "free";
const isCurrentPlan =
currentSubscription?.isPaying === undefined || currentSubscription?.isPaying === false;
let actionText = "Select plan";
if (showActionText) {
if (isCurrentPlan) {
actionText = "Current Plan";
} else {
actionText = "Downgrade";
}
}
return (
<TierContainer>
<Form action={`/resources/${organizationSlug}/subscribe`} method="post" {...form.props}>
<Header title={plan.title} cost={0} />
<TierLimit>
Up to {plan.concurrentRuns?.freeAllowance}{" "}
<DefinitionTip
title={pricingDefinitions.concurrentRuns.title}
content={pricingDefinitions.concurrentRuns.content}
>
{pricingDefinitions.concurrentRuns.title}
</DefinitionTip>
</TierLimit>
<input type="hidden" name="type" value="free" />
<div className="py-6">
{buttonPath ? (
<LinkButton
variant="secondary/large"
fullWidth
className="text-md font-medium"
to={buttonPath}
>
{actionText}
</LinkButton>
) : (
<Button
variant="secondary/large"
fullWidth
className="text-md font-medium"
disabled={isLoading || isCurrentPlan}
>
{isLoading ? (
<div className="flex items-center gap-2">
<Spinner color="white" />
Updating plan
</div>
) : (
actionText
)}
</Button>
)}
</div>
<ul className="flex flex-col gap-2.5">
<FeatureItem checked>
Up to {plan.runs?.freeAllowance ? formatNumberCompact(plan.runs.freeAllowance) : ""}{" "}
<DefinitionTip
title={pricingDefinitions.jobRuns.title}
content={pricingDefinitions.jobRuns.content}
>
{pricingDefinitions.jobRuns.title}
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.jobs.title}
content={pricingDefinitions.jobs.content}
>
jobs
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.tasks.title}
content={pricingDefinitions.tasks.content}
>
tasks
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.events.title}
content={pricingDefinitions.events.content}
>
events
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>Unlimited team members</FeatureItem>
<FeatureItem checked>24 hour log retention</FeatureItem>
<FeatureItem checked>Community support</FeatureItem>
<FeatureItem>Custom integrations</FeatureItem>
<FeatureItem>Role-based access control</FeatureItem>
<FeatureItem>SSO</FeatureItem>
<FeatureItem>On-prem option</FeatureItem>
</ul>
</Form>
</TierContainer>
);
}
export function TierPro({
plan,
organizationSlug,
showActionText,
currentSubscription,
}: {
plan: Plan;
organizationSlug: string;
showActionText: boolean;
currentSubscription?: ActiveSubscription;
}) {
const lastSubmission = useActionData();
const [form] = useForm({
id: "subscribe",
// TODO: type this
lastSubmission: lastSubmission as any,
onValidate({ formData }) {
return parse(formData, { schema: SetPlanBodySchema });
},
});
const navigation = useNavigation();
const isLoading =
(navigation.state === "submitting" || navigation.state === "loading") &&
navigation.formData?.get("planCode") === plan.code;
const currentConcurrencyTier = currentSubscription?.plan.concurrentRuns.pricing?.code;
const [concurrentBracketCode, setConcurrentBracketCode] = useState(
currentConcurrencyTier ?? plan.concurrentRuns?.pricing?.tiers[0].code
);
const concurrencyTiers = plan.concurrentRuns?.pricing?.tiers ?? [];
const selectedTier = concurrencyTiers.find((c) => c.code === concurrentBracketCode);
const freeRunCount = plan.runs?.pricing?.brackets[0].upto ?? 0;
const mostExpensiveRunCost = plan.runs?.pricing?.brackets[1]?.unitCost ?? 0;
const isCurrentPlan = currentConcurrencyTier === concurrentBracketCode;
let actionText = "Select plan";
if (showActionText) {
if (isCurrentPlan) {
actionText = "Current Plan";
} else {
const currentTierIndex = concurrencyTiers.findIndex((c) => c.code === currentConcurrencyTier);
const selectedTierIndex = concurrencyTiers.findIndex((c) => c.code === concurrentBracketCode);
actionText = currentTierIndex < selectedTierIndex ? "Upgrade" : "Downgrade";
}
}
return (
<TierContainer isHighlighted>
<Form action={`/resources/${organizationSlug}/subscribe`} method="post" {...form.props}>
<Header title={plan.title} isHighlighted cost={selectedTier?.tierCost} />
<div className="mb-2 mt-6 font-sans text-sm font-normal text-bright">
<DefinitionTip
title={pricingDefinitions.concurrentRuns.title}
content={pricingDefinitions.concurrentRuns.content}
>
{pricingDefinitions.concurrentRuns.title}
</DefinitionTip>
</div>
<input type="hidden" name="type" value="paid" />
<input type="hidden" name="planCode" value={plan.code} />
<SegmentedControl
name="concurrentRunBracket"
options={concurrencyTiers.map((c) => ({ label: `Up to ${c.upto}`, value: c.code }))}
fullWidth
value={concurrentBracketCode}
onChange={(v) => setConcurrentBracketCode(v)}
/>
<div className="py-6">
<Button
variant="primary/large"
fullWidth
className="text-md font-medium"
type="submit"
disabled={isLoading || isCurrentPlan}
>
{isLoading ? (
<div className="flex items-center gap-2">
<Spinner color="white" />
Updating plan
</div>
) : (
actionText
)}
</Button>
</div>
<ul className="flex flex-col gap-2.5">
<FeatureItem checked>
Includes {freeRunCount ? formatNumberCompact(freeRunCount) : ""}{" "}
<DefinitionTip
title={pricingDefinitions.jobRuns.title}
content={pricingDefinitions.jobRuns.content}
>
{pricingDefinitions.jobRuns.title}
</DefinitionTip>
, then{" "}
<DefinitionTip
title="Runs volume discount"
content={
<RunsVolumeDiscountTable hideHeader brackets={plan.runs?.pricing?.brackets ?? []} />
}
>
{"<"} ${(mostExpensiveRunCost * 1000).toFixed(2)}/1K runs
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.jobs.title}
content={pricingDefinitions.jobs.content}
>
jobs
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.tasks.title}
content={pricingDefinitions.tasks.content}
>
tasks
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.events.title}
content={pricingDefinitions.events.content}
>
events
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>Unlimited team members</FeatureItem>
<FeatureItem checked>7 day log retention</FeatureItem>
<FeatureItem checked>Dedicated Slack support</FeatureItem>
<FeatureItem>Custom integrations</FeatureItem>
<FeatureItem>Role-based access control</FeatureItem>
<FeatureItem>SSO</FeatureItem>
<FeatureItem>On-prem option</FeatureItem>
</ul>
</Form>
</TierContainer>
);
}
export function TierEnterprise() {
return (
<TierContainer>
<Header title="Enterprise" />
<TierLimit>
Flexible{" "}
<DefinitionTip
title={pricingDefinitions.concurrentRuns.title}
content={pricingDefinitions.concurrentRuns.content}
>
{pricingDefinitions.concurrentRuns.title}
</DefinitionTip>
</TierLimit>
<div className="py-6">
<Feedback
button={
<Button variant="secondary/large" fullWidth className="text-md font-medium">
Contact us
</Button>
}
defaultValue="enterprise"
/>
</div>
<ul className="flex flex-col gap-2.5">
<FeatureItem checked>
Flexible{" "}
<DefinitionTip
title={pricingDefinitions.jobRuns.title}
content={pricingDefinitions.jobRuns.content}
>
{pricingDefinitions.jobRuns.title}
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.jobs.title}
content={pricingDefinitions.jobs.content}
>
jobs
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.tasks.title}
content={pricingDefinitions.tasks.content}
>
tasks
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>
Unlimited{" "}
<DefinitionTip
title={pricingDefinitions.events.title}
content={pricingDefinitions.events.content}
>
events
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>Unlimited team members</FeatureItem>
<FeatureItem checked>30 day log retention</FeatureItem>
<FeatureItem checked>Priority support</FeatureItem>
<FeatureItem checked>
Custom{" "}
<DefinitionTip
title={pricingDefinitions.integrations.title}
content={pricingDefinitions.integrations.content}
>
{pricingDefinitions.integrations.title}
</DefinitionTip>
</FeatureItem>
<FeatureItem checked>Role-based access control</FeatureItem>
<FeatureItem checked>SSO</FeatureItem>
<FeatureItem checked>On-prem option</FeatureItem>
</ul>
</TierContainer>
);
}
function TierContainer({
children,
isHighlighted,
}: {
children: React.ReactNode;
isHighlighted?: boolean;
}) {
return (
<div
className={cn(
"flex w-full min-w-[16rem] flex-col rounded-md border p-6",
isHighlighted ? "border-indigo-500" : "border-border"
)}
>
{children}
</div>
);
}
function Header({
title,
cost: flatCost,
isHighlighted,
}: {
title: string;
cost?: number;
isHighlighted?: boolean;
}) {
return (
<div className="flex flex-col gap-2">
<h2 className={cn("text-xl font-medium", isHighlighted ? "text-indigo-500" : "text-dimmed")}>
{title}
</h2>
{flatCost === 0 || flatCost ? (
<h3 className="text-4xl font-medium">
${flatCost}
<span className="text-2sm font-normal tracking-wide text-dimmed">/month</span>
</h3>
) : (
<h2 className="text-4xl font-medium">Custom</h2>
)}
</div>
);
}
function TierLimit({ children }: { children: React.ReactNode }) {
return (
<div>
<hr className="my-[1.9rem]" />
<div className="mb-[0.6rem] mt-6 font-sans text-sm font-normal text-bright">{children}</div>
</div>
);
}
function FeatureItem({ checked, children }: { checked?: boolean; children: React.ReactNode }) {
return (
<li className="flex items-center gap-2">
{checked ? (
<CheckIcon className="h-4 w-4 text-green-500" />
) : (
<XMarkIcon className="h-4 w-4 text-slate-500" />
)}
<div className={cn("font-sans text-sm font-normal", checked ? "text-bright" : "text-dimmed")}>
{children}
</div>
</li>
);
}
@@ -0,0 +1,64 @@
import { RunPriceBracket } from "@trigger.dev/billing";
import { Header2 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
import { formatNumberCompact } from "~/utils/numberFormatter";
export function RunsVolumeDiscountTable({
className,
hideHeader = false,
brackets,
}: {
className?: string;
hideHeader?: boolean;
brackets: RunPriceBracket[];
}) {
const runsVolumeDiscountRow =
"flex justify-between whitespace-nowrap border-b gap-16 border-border last:pb-0 last:border-none py-2";
const bracketData = bracketInfo(brackets);
return (
<div className={className}>
{hideHeader ? null : <Header2 className="mb-2">Runs volume discount</Header2>}
<ul>
{bracketData.map((bracket, index) => (
<li key={index} className={runsVolumeDiscountRow}>
<Paragraph variant="small">{bracket.range}</Paragraph>
<Paragraph variant="small">{bracket.costLabel}</Paragraph>
</li>
))}
</ul>
</div>
);
}
function bracketInfo(brackets: RunPriceBracket[]) {
return brackets.map((bracket, index) => {
const { upto, unitCost } = bracket;
if (index === 0) {
return {
range: `First ${formatNumberCompact(upto!)}/mo`,
costLabel: "Free",
};
}
const from = brackets[index - 1].upto;
const fromFormatted = formatNumberCompact(from!);
const toFormatted = upto ? formatNumberCompact(upto) : undefined;
const costLabel = `$${(unitCost * 1000).toFixed(2)}/1,000`;
if (!upto) {
return {
range: `${fromFormatted} +`,
costLabel,
};
}
return {
range: `${fromFormatted}${toFormatted}`,
costLabel,
};
});
}
@@ -0,0 +1,43 @@
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { formatNumberCompact } from "~/utils/numberFormatter";
import { plansPath } from "~/utils/pathBuilder";
import { LinkButton } from "../primitives/Buttons";
import { Paragraph } from "../primitives/Paragraph";
import { Callout } from "../primitives/Callout";
type UpgradePromptProps = {
organization: MatchedOrganization;
};
export function UpgradePrompt({ organization }: UpgradePromptProps) {
const currentPlan = useCurrentPlan();
if (!currentPlan || !currentPlan.usage.exceededRunCount || !currentPlan.usage.runCountCap) {
return null;
}
return (
<Callout variant="error" className="flex h-full items-center rounded-none px-1 py-0">
<div className="flex items-center justify-between gap-3">
<Paragraph variant="extra-small" className="text-white">
{organization.runsEnabled
? `You have exceeded the monthly ${formatNumberCompact(
currentPlan.usage.runCountCap
)} runs
limit`
: `No runs are executing because you have exceeded the free limit`}
</Paragraph>
<LinkButton
variant={"primary/small"}
LeadingIcon={ArrowUpCircleIcon}
leadingIconClassName="px-0"
to={plansPath(organization)}
>
Upgrade
</LinkButton>
</div>
</Callout>
);
}
@@ -0,0 +1,167 @@
import { cn } from "~/utils/cn";
import { formatNumberCompact } from "~/utils/numberFormatter";
import { Paragraph } from "../primitives/Paragraph";
import { SimpleTooltip } from "../primitives/Tooltip";
import { motion } from "framer-motion";
type UsageBarProps = {
numberOfCurrentRuns: number;
billingLimit?: number;
tierRunLimit?: number;
projectedRuns: number;
subscribedToPaidTier?: boolean;
};
export function UsageBar({
numberOfCurrentRuns,
billingLimit,
tierRunLimit,
projectedRuns,
subscribedToPaidTier = false,
}: UsageBarProps) {
const getLargestNumber = Math.max(
numberOfCurrentRuns,
tierRunLimit ?? -Infinity,
projectedRuns,
billingLimit ?? -Infinity
);
//creates a maximum range for the progress bar, add 10% to the largest number so the bar doesn't reach the end
const maxRange = Math.round(getLargestNumber * 1.1);
const tierRunLimitPercentage = tierRunLimit ? Math.round((tierRunLimit / maxRange) * 100) : 0;
const projectedRunsPercentage = Math.round((projectedRuns / maxRange) * 100);
const billingLimitPercentage =
billingLimit !== undefined ? Math.round((billingLimit / maxRange) * 100) : 0;
const usagePercentage = Math.round((numberOfCurrentRuns / maxRange) * 100);
//cap the usagePercentage to the freeRunLimitPercentage
const usageCappedToLimitPercentage = Math.min(usagePercentage, tierRunLimitPercentage);
return (
<div className="h-fit w-full py-16">
<div className="relative h-3 w-full rounded-sm bg-slate-800">
{billingLimit && (
<motion.div
initial={{ width: 0 }}
animate={{ width: billingLimitPercentage + "%" }}
transition={{ duration: 1.5, type: "spring" }}
style={{ width: `${billingLimitPercentage}%` }}
className="absolute h-3 rounded-l-sm"
>
<Legend
text="Billing limit:"
value={formatNumberCompact(billingLimit)}
position="bottomRow2"
percentage={billingLimitPercentage}
tooltipContent={`Billing limit: ${formatNumberCompact(billingLimit)}`}
/>
</motion.div>
)}
{tierRunLimit && (
<motion.div
initial={{ width: 0 }}
animate={{ width: tierRunLimitPercentage + "%" }}
transition={{ duration: 1.5, type: "spring" }}
style={{ width: `${tierRunLimitPercentage}%` }}
className="absolute h-3 rounded-l-sm bg-green-900/50"
>
<Legend
text={`${subscribedToPaidTier ? "Included free:" : "Free tier limit:"}`}
value={formatNumberCompact(tierRunLimit)}
position="bottomRow1"
percentage={tierRunLimitPercentage}
tooltipContent={`${
subscribedToPaidTier
? `Runs included free: ${formatNumberCompact(tierRunLimit)}`
: `Free tier runs limit: ${formatNumberCompact(tierRunLimit)}`
}`}
/>
</motion.div>
)}
{projectedRuns !== 0 && (
<motion.div
initial={{ width: 0 }}
animate={{ width: projectedRunsPercentage + "%" }}
transition={{ duration: 1.5, type: "spring" }}
style={{ width: `${projectedRunsPercentage}%` }}
className="absolute h-3 rounded-l-sm"
>
<Legend
text="Projected:"
value={formatNumberCompact(projectedRuns)}
position="topRow2"
percentage={projectedRunsPercentage}
tooltipContent={`Projected runs: ${formatNumberCompact(projectedRuns)}`}
/>
</motion.div>
)}
<motion.div
initial={{ width: 0 }}
animate={{ width: usagePercentage + "%" }}
transition={{ duration: 1.5, type: "spring" }}
style={{ width: `${usagePercentage}%` }}
className={cn(
"absolute h-3 rounded-l-sm",
subscribedToPaidTier ? "bg-green-600" : "bg-rose-600"
)}
>
<Legend
text="Current:"
value={formatNumberCompact(numberOfCurrentRuns)}
position="topRow1"
percentage={usagePercentage}
tooltipContent={`Current run count: ${formatNumberCompact(numberOfCurrentRuns)}`}
/>
</motion.div>
<motion.div
initial={{ width: 0 }}
animate={{ width: usageCappedToLimitPercentage + "%" }}
transition={{ duration: 1.5, type: "spring" }}
style={{ width: `${usageCappedToLimitPercentage}%` }}
className="absolute h-3 rounded-l-sm bg-green-600"
/>
</div>
</div>
);
}
const positions = {
topRow1: "bottom-0 h-9",
topRow2: "bottom-0 h-14",
bottomRow1: "top-0 h-9 items-end",
bottomRow2: "top-0 h-14 items-end",
};
type LegendProps = {
text: string;
value: number | string;
percentage: number;
position: keyof typeof positions;
tooltipContent: string;
};
function Legend({ text, value, position, percentage, tooltipContent }: LegendProps) {
const flipLegendPositionValue = 80;
const flipLegendPosition = percentage > flipLegendPositionValue ? true : false;
return (
<div
className={cn(
"absolute left-full z-10 flex border-slate-400",
positions[position],
flipLegendPosition === true ? "-translate-x-full border-r" : "border-l"
)}
>
<SimpleTooltip
button={
<Paragraph className="mr-px h-fit whitespace-nowrap bg-background px-1.5 text-xs text-dimmed">
{text}
<span className="ml-1 text-bright">{value}</span>
</Paragraph>
}
variant="dark"
side="top"
content={tooltipContent}
className="z-50 h-fit"
/>
</div>
);
}
@@ -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";
}
}
@@ -5,9 +5,11 @@ type Environment = Pick<RuntimeEnvironment, "type">;
export function EnvironmentLabel({
environment,
userName,
className,
}: {
environment: Environment;
userName?: string;
className?: string;
}) {
return (
@@ -18,19 +20,19 @@ export function EnvironmentLabel({
className
)}
>
{environmentTitle(environment)}
{environmentTitle(environment, userName)}
</span>
);
}
export function environmentTitle(environment: Environment) {
export function environmentTitle(environment: Environment, username?: string) {
switch (environment.type) {
case "PRODUCTION":
return "Prod";
case "STAGING":
return "Staging";
case "DEVELOPMENT":
return "Dev";
return username ? `Dev: ${username}` : "Dev";
case "PREVIEW":
return "Preview";
}
@@ -0,0 +1,105 @@
import { ArrowPathIcon } from "@heroicons/react/20/solid";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import { useFetcher } from "@remix-run/react";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
import { generateTwoRandomWords } from "~/utils/randomWords";
import { Button } from "../primitives/Buttons";
import { Header1 } from "../primitives/Headers";
import { Input } from "../primitives/Input";
import { Paragraph } from "../primitives/Paragraph";
import { Spinner } from "../primitives/Spinner";
type ModalProps = {
id: string;
title: string;
};
type ModalContentProps = ModalProps & {
randomWord: string;
closeModal: () => void;
};
export function RegenerateApiKeyModal({ id, title }: ModalProps) {
const randomWord = generateTwoRandomWords();
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="tertiary/small"
leadingIconClassName="text-dimmed"
LeadingIcon={ArrowPathIcon}
>
Regenerate
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>{`Regenerate ${title} Environment Key`}</DialogHeader>
<RegenerateApiKeyModalContent
id={id}
title={title}
randomWord={randomWord}
closeModal={() => setOpen(false)}
/>
</DialogContent>
</Dialog>
);
}
const RegenerateApiKeyModalContent = ({ id, randomWord, title, closeModal }: ModalContentProps) => {
const [confirmationText, setConfirmationText] = useState("");
const fetcher = useFetcher();
const isSubmitting = fetcher.state === "submitting";
// form submission completed
if (fetcher.state === "loading") {
closeModal();
}
return (
<div className="flex w-full flex-col items-center gap-y-5 pb-4">
<div className="mt-3 flex gap-x-3 rounded-md border border-ui-border py-4 pl-3 pr-5">
<ExclamationTriangleIcon className="relative top-1 h-6 w-6 min-w-[2rem] text-amber-500" />
<Paragraph>
Regenerating the keys for this environment will temporarily break any live Jobs in the
<span className="text-bright"> {title} Environment</span> until the new API keys are set
in the relevant environment variables.
</Paragraph>
</div>
<fetcher.Form
method="post"
action={`/resources/environments/${id}/regenerate-api-key`}
className="mt-2 flex w-full flex-col items-center gap-2"
>
<div className="mb-4 flex items-center gap-x-2">
<Paragraph variant="small/bright">Enter this text below to confirm:</Paragraph>
<Paragraph
variant="small"
className="select-all rounded-md border border-ui-border bg-slate-900 px-2 py-1 font-mono text-bright"
>
{randomWord}
</Paragraph>
</div>
<div className="flex items-center">
<Input
type="text"
placeholder="Confirmation text"
fullWidth
value={confirmationText}
onChange={(e) => setConfirmationText(e.target.value)}
className="rounded-r-none"
variant="large"
/>
<Button
variant="primary/large"
disabled={confirmationText !== randomWord}
className="rounded-l-none"
>
{isSubmitting ? <Spinner color="white" /> : <>Regenerate</>}
</Button>
</div>
</fetcher.Form>
</div>
);
};
@@ -0,0 +1,45 @@
import { CodeBlock } from "../code/CodeBlock";
import { DateTime } from "../primitives/DateTime";
import { Header3 } from "../primitives/Headers";
import {
RunPanel,
RunPanelBody,
RunPanelDivider,
RunPanelIconProperty,
RunPanelIconSection,
} from "~/components/run/RunCard";
import { Event } from "~/presenters/EventPresenter.server";
export function EventDetail({ event }: { event: Event }) {
const { id, name, payload, context, timestamp, deliveredAt } = event;
return (
<RunPanel selected={false}>
<RunPanelBody>
<RunPanelIconSection>
<RunPanelIconProperty
icon="calendar"
label="Created"
value={<DateTime date={timestamp} />}
/>
{deliveredAt && (
<RunPanelIconProperty
icon="flag"
label="Delivered"
value={<DateTime date={deliveredAt} />}
/>
)}
<RunPanelIconProperty icon="id" label="Event name" value={name} />
<RunPanelIconProperty icon="account" label="Event ID" value={id} />
</RunPanelIconSection>
<RunPanelDivider />
<div className="mt-4 flex flex-col gap-2">
<Header3>Payload</Header3>
<CodeBlock code={payload} />
<Header3>Context</Header3>
<CodeBlock code={context} />
</div>
</RunPanelBody>
</RunPanel>
);
}
@@ -0,0 +1,16 @@
import { z } from "zod";
import { DirectionSchema, FilterableEnvironment } from "~/components/runs/RunStatuses";
export const EventListSearchSchema = z.object({
cursor: z.string().optional(),
direction: DirectionSchema.optional(),
environment: FilterableEnvironment.optional(),
from: z
.string()
.transform((value) => parseInt(value))
.optional(),
to: z
.string()
.transform((value) => parseInt(value))
.optional(),
});
@@ -0,0 +1,104 @@
import { useNavigate } from "@remix-run/react";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
import { Paragraph } from "../primitives/Paragraph";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../primitives/Select";
import { EventListSearchSchema } from "./EventStatuses";
import { environmentKeys, FilterableEnvironment } from "~/components/runs/RunStatuses";
import { TimeFrameFilter } from "../runs/TimeFrameFilter";
import { useCallback } from "react";
import { Button } from "../primitives/Buttons";
export function EventsFilters() {
const navigate = useNavigate();
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const { environment, from, to } = EventListSearchSchema.parse(
Object.fromEntries(searchParams.entries())
);
const handleFilterChange = useCallback((filterType: string, value: string | undefined) => {
if (value) {
searchParams.set(filterType, value);
} else {
searchParams.delete(filterType);
}
searchParams.delete("cursor");
searchParams.delete("direction");
navigate(`${location.pathname}?${searchParams.toString()}`);
}, []);
const handleTimeFrameChange = useCallback((range: { from?: number; to?: number }) => {
if (range.from) {
searchParams.set("from", range.from.toString());
} else {
searchParams.delete("from");
}
if (range.to) {
searchParams.set("to", range.to.toString());
} else {
searchParams.delete("to");
}
searchParams.delete("cursor");
searchParams.delete("direction");
navigate(`${location.pathname}?${searchParams.toString()}`);
}, []);
const handleEnvironmentChange = (value: FilterableEnvironment | "ALL") => {
handleFilterChange("environment", value === "ALL" ? undefined : value);
};
const clearFilters = useCallback(() => {
searchParams.delete("status");
searchParams.delete("environment");
searchParams.delete("from");
searchParams.delete("to");
navigate(`${location.pathname}?${searchParams.toString()}`);
}, []);
return (
<div className="flex flex-row justify-between gap-x-2">
<SelectGroup>
<Select
name="environment"
value={environment ?? "ALL"}
onValueChange={handleEnvironmentChange}
>
<SelectTrigger size="secondary/small" width="full">
<SelectValue placeholder={"Select environment"} className="ml-2 p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph variant="extra-small" className="pl-0.5">
All environments
</Paragraph>
</SelectItem>
{environmentKeys.map((env) => (
<SelectItem key={env} value={env}>
<div className="flex items-center gap-x-2">
<EnvironmentLabel environment={{ type: env }} />
<Paragraph variant="extra-small">environment</Paragraph>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<TimeFrameFilter from={from} to={to} onRangeChanged={handleTimeFrameChange} />
<Button variant="tertiary/small" onClick={() => clearFilters()} LeadingIcon={"close"}>
Clear
</Button>
</div>
);
}
@@ -0,0 +1,134 @@
import { StopIcon } from "@heroicons/react/24/outline";
import { CheckIcon } from "@heroicons/react/24/solid";
import { RuntimeEnvironmentType, User } from "@trigger.dev/database";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
import { DateTime } from "../primitives/DateTime";
import { Paragraph } from "../primitives/Paragraph";
import { Spinner } from "../primitives/Spinner";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellChevron,
TableHeader,
TableHeaderCell,
TableRow,
} from "../primitives/Table";
type EventTableItem = {
id: string;
name: string | null;
environment: {
type: RuntimeEnvironmentType;
userId?: string;
userName?: string;
};
createdAt: Date | null;
isTest: boolean;
deliverAt: Date | null;
deliveredAt: Date | null;
cancelledAt: Date | null;
runs: number;
};
type EventsTableProps = {
total: number;
hasFilters: boolean;
events: EventTableItem[];
isLoading?: boolean;
eventsParentPath: string;
currentUser: User;
};
export function EventsTable({
total,
hasFilters,
events,
isLoading = false,
eventsParentPath,
currentUser,
}: EventsTableProps) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Event</TableHeaderCell>
<TableHeaderCell>Env</TableHeaderCell>
<TableHeaderCell>Received Time</TableHeaderCell>
<TableHeaderCell>Delivery Time</TableHeaderCell>
<TableHeaderCell>Delivered</TableHeaderCell>
<TableHeaderCell>Canceled Time</TableHeaderCell>
<TableHeaderCell>Test</TableHeaderCell>
<TableHeaderCell>Runs</TableHeaderCell>
<TableHeaderCell>
<span className="sr-only">Go to page</span>
</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{total === 0 && !hasFilters ? (
<TableBlankRow colSpan={9}>
<NoEvents title="No events found" />
</TableBlankRow>
) : events.length === 0 ? (
<TableBlankRow colSpan={9}>
<NoEvents title="No events match your filters" />
</TableBlankRow>
) : (
events.map((event) => {
const path = `${eventsParentPath}/events/${event.id}`;
const usernameForEnv =
currentUser.id !== event.environment.userId ? event.environment.userName : undefined;
return (
<TableRow key={event.id}>
<TableCell to={path}>{typeof event.name === "string" ? event.name : "-"}</TableCell>
<TableCell to={path}>
<EnvironmentLabel environment={event.environment} userName={usernameForEnv} />
</TableCell>
<TableCell to={path}>
{event.createdAt ? <DateTime date={event.createdAt} /> : ""}
</TableCell>
<TableCell to={path}>
{event.deliverAt ? <DateTime date={event.deliverAt} /> : ""}
</TableCell>
<TableCell to={path}>
{event.deliveredAt ? <DateTime date={event.deliveredAt} /> : ""}
</TableCell>
<TableCell to={path}>
{event.cancelledAt ? <DateTime date={event.cancelledAt} /> : ""}
</TableCell>
<TableCell to={path}>
{event.isTest ? (
<CheckIcon className="h-4 w-4 text-slate-400" />
) : (
<StopIcon className="h-4 w-4 text-slate-850" />
)}
</TableCell>
<TableCell to={path}>{event.runs}</TableCell>
<TableCellChevron to={path} isSticky />
</TableRow>
);
})
)}
{isLoading && (
<TableBlankRow
colSpan={8}
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-slate-900/90"
>
<Spinner /> <span className="text-dimmed">Loading</span>
</TableBlankRow>
)}
</TableBody>
</Table>
);
}
function NoEvents({ title }: { title: string }) {
return (
<div className="flex items-center justify-center">
<Paragraph className="w-auto">{title}</Paragraph>
</div>
);
}
@@ -3,11 +3,10 @@ import { GitHubDarkIcon } from "@trigger.dev/companyicons";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { projectSetupPath } from "~/utils/pathBuilder";
import { PageGradient } from "../PageGradient";
import { LinkButton } from "../primitives/Buttons";
import { Header1 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
export type FrameworkComingSoonProps = {
frameworkName: string;
githubIssueUrl: string;
@@ -25,43 +24,41 @@ export function FrameworkComingSoon({
const project = useProject();
return (
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="mb-6 grid place-items-center pb-6">{children}</div>
<div className="flex items-center justify-between">
<Header1 spacing>{frameworkName} is coming soon!</Header1>
<LinkButton
to={projectSetupPath(organization, project)}
variant="tertiary/small"
LeadingIcon={Squares2X2Icon}
>
Choose a different framework
</LinkButton>
</div>
<Paragraph spacing className="border-b border-slate-800 pb-4">
We're working hard to bring support for {frameworkName} in Trigger.dev. Follow along with
the GitHub issue or contribute and help us bring it to Trigger.dev faster.
</Paragraph>
<a
href={githubIssueUrl}
target="_blank"
className="group mt-4 block max-w-sm rounded-md bg-bright px-10 py-8 transition hover:bg-slate-100"
<div className="mx-auto max-w-3xl pt-16">
<div className="mb-6 grid place-items-center pb-6">{children}</div>
<div className="flex items-center justify-between">
<Header1 spacing>{frameworkName} is coming soon!</Header1>
<LinkButton
to={projectSetupPath(organization, project)}
variant="tertiary/small"
LeadingIcon={Squares2X2Icon}
>
<Paragraph spacing>triggerdotdev/trigger.dev</Paragraph>
<h2 className="text-2xl font-semibold text-black">
<span className="mr-1 font-normal text-dimmed">#{githubIssueNumber}</span>Framework:
support for {frameworkName}
</h2>
<div className="mt-4 flex items-center gap-1.5">
<GitHubDarkIcon className="h-4 w-4" />
<Paragraph variant="small">View on GitHub</Paragraph>
<NamedIcon
name="arrow-right"
className="h-4 w-4 text-dimmed transition group-hover:translate-x-1"
/>
</div>
</a>
Choose a different framework
</LinkButton>
</div>
</PageGradient>
<Paragraph spacing className="border-b border-slate-800 pb-4">
We're working hard to bring support for {frameworkName} in Trigger.dev. Follow along with
the GitHub issue or contribute and help us bring it to Trigger.dev faster.
</Paragraph>
<a
href={githubIssueUrl}
target="_blank"
className="group mt-4 block max-w-sm rounded-md bg-bright px-10 py-8 transition hover:bg-slate-100"
>
<Paragraph spacing>triggerdotdev/trigger.dev</Paragraph>
<h2 className="text-2xl font-semibold text-black">
<span className="mr-1 font-normal text-dimmed">#{githubIssueNumber}</span>Framework:
support for {frameworkName}
</h2>
<div className="mt-4 flex items-center gap-1.5">
<GitHubDarkIcon className="h-4 w-4" />
<Paragraph variant="small">View on GitHub</Paragraph>
<NamedIcon
name="arrow-right"
className="h-4 w-4 text-dimmed transition group-hover:translate-x-1"
/>
</div>
</a>
</div>
);
}
@@ -24,7 +24,6 @@ import {
projectSetupSvelteKitPath,
} from "~/utils/pathBuilder";
import { Feedback } from "../Feedback";
import { PageGradient } from "../PageGradient";
import { Button } from "../primitives/Buttons";
import { Header1 } from "../primitives/Headers";
@@ -33,51 +32,49 @@ export function FrameworkSelector() {
const project = useProject();
return (
<PageGradient>
<div className="mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<Header1 spacing>Choose a framework to get started</Header1>
<Feedback
button={
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
Request a framework
</Button>
}
defaultValue="feature"
/>
</div>
<div className="grid grid-cols-3 place-items-center gap-2">
<FrameworkLink to={projectSetupNextjsPath(organization, project)} supported>
<NextjsLogo className="w-32" />
</FrameworkLink>
<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={projectSetupNuxtPath(organization, project)}>
<NuxtLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupSvelteKitPath(organization, project)}>
<SvelteKitLogo className="w-44" />
</FrameworkLink>
<FrameworkLink to={projectSetupFastifyPath(organization, project)}>
<FastifyLogo className="w-36" />
</FrameworkLink>
<FrameworkLink to={projectSetupNestjsPath(organization, project)} supported>
<NestjsLogo className="w-36" />
</FrameworkLink>
</div>
<div className="mx-auto max-w-3xl pt-28">
<div className="flex items-center justify-between">
<Header1 spacing>Choose a framework to get started</Header1>
<Feedback
button={
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
Request a framework
</Button>
}
defaultValue="feature"
/>
</div>
</PageGradient>
<div className="grid grid-cols-3 place-items-center gap-2">
<FrameworkLink to={projectSetupNextjsPath(organization, project)} supported>
<NextjsLogo className="w-32" />
</FrameworkLink>
<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={projectSetupSvelteKitPath(organization, project)} supported>
<SvelteKitLogo className="w-44" />
</FrameworkLink>
<FrameworkLink to={projectSetupNestjsPath(organization, project)} supported>
<NestjsLogo className="w-36" />
</FrameworkLink>
<FrameworkLink to={projectSetupFastifyPath(organization, project)}>
<FastifyLogo className="w-36" />
</FrameworkLink>
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
<NuxtLogo className="w-32" />
</FrameworkLink>
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
<RedwoodLogo className="w-44" />
</FrameworkLink>
</div>
</div>
);
}
@@ -93,8 +90,7 @@ function FrameworkLink({ children, to, supported = false }: FrameworkLinkProps)
to={to}
className={cn(
"flex h-28 w-full items-center justify-center rounded-md border border-slate-750 px-8 py-4 transition hover:bg-slate-850",
!supported &&
"border-dashed opacity-50 grayscale transition hover:opacity-100 hover:grayscale-0"
!supported && "border opacity-30 grayscale transition hover:opacity-100 hover:grayscale-0"
)}
>
{children}
@@ -4,8 +4,7 @@ import { StepNumber } from "~/components/primitives/StepNumber";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
import { jobTestPath } from "~/utils/pathBuilder";
import { docsPath, jobTestPath } from "~/utils/pathBuilder";
import { CodeBlock } from "../code/CodeBlock";
import { InlineCode } from "../code/InlineCode";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
@@ -21,6 +20,7 @@ import selectEnvironment from "./select-environment.png";
import selectExample from "./select-example.png";
import { StepContentContainer } from "../StepContentContainer";
import { TriggerDevCommand } from "../SetupCommands";
import { IntegrationIcon } from "~/assets/icons/IntegrationIcon";
export function HowToRunYourJob() {
const organization = useOrganization();
@@ -294,3 +294,140 @@ export function HowToUseApiKeysAndEndpoints() {
</>
);
}
export function WhatAreHttpEndpoints() {
return (
<>
<Paragraph spacing>
HTTP endpoints allow you to trigger your Jobs from any webhooks. They require a bit more
work than using <TextLink to={docsPath("integrations/introduction")}>Integrations</TextLink>{" "}
but allow you to connect to any API.
</Paragraph>
<Header2 spacing>Getting started</Header2>
<Paragraph spacing>
You need to define the HTTP endpoint in your code. To do this you use{" "}
<InlineCode>client.defineHttpEndpoint()</InlineCode>. This will create an HTTP endpoint.
</Paragraph>
<Paragraph spacing>
Then you can create a Trigger from this by calling <InlineCode>.onRequest()</InlineCode> on
the created HTTP endpoint.
</Paragraph>
<Callout variant="docs" to={docsPath("documentation/concepts/http-endpoints")}>
Read the HTTP endpoints guide to learn more.
</Callout>
<Header2 spacing className="mt-4">
An example: cal.com
</Header2>
<CodeBlock
code={`//create an HTTP endpoint
const caldotcom = client.defineHttpEndpoint({
id: "cal.com",
source: "cal.com",
icon: "caldotcom",
verify: async (request) => {
//this helper function makes verifying most webhooks easy
return await verifyRequestSignature({
request,
headerName: "X-Cal-Signature-256",
secret: process.env.CALDOTCOM_SECRET!,
algorithm: "sha256",
});
},
});
client.defineJob({
id: "http-caldotcom",
name: "HTTP Cal.com",
version: "1.0.0",
enabled: true,
//create a Trigger from the HTTP endpoint above. The filter is optional.
trigger: caldotcom.onRequest({ filter: { body: { triggerEvent: ["BOOKING_CANCELLED"] } } }),
run: async (request, io, ctx) => {
//note that when using HTTP endpoints, the first parameter is the request
//you need to get the body, usually it will be json so you do:
const body = await request.json();
await io.logger.info("Body", body);
},
});`}
/>
</>
);
}
export function HowToConnectHttpEndpoint() {
return (
<>
<Header2 spacing>Setting up your webhook</Header2>
<Paragraph spacing>
To start receiving data you need to enter the Endpoint URL and secret into the API service
you want to receive webhooks from.
</Paragraph>
<StepNumber stepNumber="1" title={<>Go to the relevant API dashboard</>} />
<StepContentContainer>
<Paragraph spacing>
For example, if you want to receive webhooks from Cal.com then you should login to your
Cal.com account and go to their Settings/Developer/Webhooks page.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title={<>Copy the Webhook URL and Secret</>} />
<StepContentContainer>
<Paragraph spacing>
A unique Webhook URL is created for each environment (Dev, Staging, and Prod). Jobs will
only be triggered from the relevant environment.
</Paragraph>
<Paragraph spacing>
Copy the relevant Endpoint URL and secret from the table opposite and paste it into the
correct place in the API dashboard you located in the previous step.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="3" title={<>Add the Secret to your Environment variables</>} />
<StepContentContainer>
<Paragraph spacing>
You should also add the Secret to the Environment variables in your code and where you're
deploying. Usually in Node this means adding it to the .env file.
</Paragraph>
<Paragraph spacing>
Use the secret in the <InlineCode>verify()</InlineCode> function of HTTP Endpoint. This
ensures that someone can't just send a request to your Endpoint and trigger a Job.
Different APIs do this verification in different ways a common way is to have a header
that has a hash of the payload and secret. Refer to the API's documentation for more
information.
</Paragraph>
</StepContentContainer>
<Header2 spacing>Triggering runs</Header2>
<StepNumber stepNumber="1" title="Ensure you're using the HTTP Endpoint in your code" />
<StepContentContainer>
<Paragraph spacing>
In your code, you should use the <InlineCode>.onRequest()</InlineCode> function in a Job
Trigger. You can filter so only data that matches your criteria triggers the Job.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Make sure your code is deployed (for Staging and Prod)" />
<StepContentContainer>
<Paragraph spacing>
If you're using the Staging or Prod environment, you need to make sure your code is
deployed. Deploy like you normally would {" "}
<TextLink to={docsPath("documentation/guides/deployment")}>
read our deployment guide
</TextLink>
.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="3" title="Perform an action that sends a webhook" />
<StepContentContainer>
<Paragraph spacing>
Now you need to actually perform an action on that third-party service that triggers the
webhook you've subscribed to. For example, add a new meeting using Cal.com.
</Paragraph>
</StepContentContainer>
<Callout variant="docs" to={docsPath("documentation/concepts/http-endpoints")}>
Read the HTTP endpoints guide to learn more.
</Callout>
</>
);
}
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { ApiAuthenticationMethodApiKey, Integration } from "~/services/externalApis/types";
import { docsIntegrationPath } from "~/utils/pathBuilder";
import { Integration } from "~/services/externalApis/types";
import { apiReferencePath, docsIntegrationPath } from "~/utils/pathBuilder";
import { LinkButton } from "../primitives/Buttons";
import { Header1, Header2 } from "../primitives/Headers";
import { NamedIconInBox } from "../primitives/NamedIcon";
@@ -48,6 +48,15 @@ export function ConnectToIntegrationSheet({
<Paragraph variant="small">{integration.description}</Paragraph>
)}
</div>
<LinkButton
to={apiReferencePath(integration.identifier)}
variant="secondary/small"
TrailingIcon="arrow-up-right"
trailingIconClassName="h-4 w-4 text-slate-400"
target="_blank"
>
View examples
</LinkButton>
<LinkButton
to={docsIntegrationPath(integration.identifier)}
variant="secondary/small"
@@ -83,13 +92,6 @@ export function ConnectToIntegrationSheet({
variant="description"
/>
)}
<RadioGroupItem
id="custom"
value="custom"
label="Fetch/Existing SDK"
description={`Alternatively, use ${integration.name} without our integration.`}
variant="description"
/>
</RadioGroup>
{integrationMethod && (
@@ -132,7 +134,5 @@ function SelectedIntegrationMethod({
callbackUrl={callbackUrl}
/>
);
case "custom":
return <CustomHelp name={integration.name} />;
}
}
@@ -46,7 +46,8 @@ export function ConnectToOAuthForm({
const [form, { title, slug, scopes, hasCustomClient, customClientId, customClientSecret }] =
useForm({
lastSubmission: fetcher.data,
// TODO: type this
lastSubmission: fetcher.data as any,
shouldRevalidate: "onSubmit",
onValidate({ formData }) {
return parse(formData, {
@@ -126,7 +127,7 @@ export function ConnectToOAuthForm({
id="hasCustomClient"
label="Use my OAuth App"
variant="simple/small"
disabled={requiresCustomOAuthApp}
readOnly={requiresCustomOAuthApp}
onChange={(checked) => setUseMyOAuthApp(checked)}
{...conform.input(hasCustomClient, { type: "checkbox" })}
defaultChecked={requiresCustomOAuthApp}
@@ -134,8 +135,9 @@ export function ConnectToOAuthForm({
{useMyOAuthApp && (
<div className="ml-6 mt-2">
<Paragraph variant="small" className="mb-2">
Set the callback url to <CodeBlock code={callbackUrl} showLineNumbers={false} />
Set the callback url to
</Paragraph>
<CodeBlock code={callbackUrl} showLineNumbers={false} />
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<InputGroup fullWidth>
@@ -1,79 +1,86 @@
import { CodeBlock } from "../code/CodeBlock";
import { Header1, Header2 } from "../primitives/Headers";
import { useState } from "react";
import { CodeExample } from "~/routes/resources.codeexample";
import { Api } from "~/services/externalApis/apis.server";
import { cn } from "~/utils/cn";
import { Feedback } from "../Feedback";
import { Header1, Header2, Header3 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
import { TextLink } from "../primitives/TextLink";
export function CustomHelp({ api }: { api: Api }) {
const [selectedExample, setSelectedExample] = useState(0);
const changeCodeExample = (index: number) => {
setSelectedExample(index);
};
export function CustomHelp({ name }: { name: string }) {
return (
<div className="mt-4">
<Header1 className="mb-2">You can use any API with requests or an SDK</Header1>
<Header2 className="mb-2">How to use an SDK</Header2>
<Header1 className="mb-2">Using {api.name} with an SDK or requests</Header1>
<Paragraph spacing>
You can call SDK methods from inside the run function, but you should wrap them in a Task to
make sure they're resumable.
You can use Trigger.dev with any existing Node SDK or even just using fetch. You can
subscribe to any API with{" "}
<TextLink href="https://trigger.dev/docs/documentation/concepts/http-endpoints">
HTTP endpoints
</TextLink>{" "}
and perform actions by wrapping tasks using{" "}
<TextLink
href="https://trigger.dev/docs/documentation/guides/writing-jobs-step-by-step#create-your-own-tasks"
className="font-mono"
>
io.runTask
</TextLink>
. This makes your background job resumable and appear in our dashboard.
</Paragraph>
<Paragraph spacing>Here's an example with the official GitHub SDK</Paragraph>
<CodeBlock
code={`
client.defineJob({
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
trigger: cronTrigger({
cron: "*/5 * * * *", // every 5 minutes
}),
run: async (payload, io, ctx) => {
//wrap an SDK call in io.runTask so it's resumable and displays in logs
const repo = await io.runTask(
"Get repo",
async () => {
//this is the regular GitHub SDK
const response = await octokit.rest.repos.get({
owner: "triggerdotdev",
repo: "trigger.dev",
});
return response.data;
},
//you can add metadata to the task to improve the display in the logs
{ name: "Get repo", icon: "github" }
);
},
});
`}
highlightedRanges={[[9, 22]]}
className="mb-4"
/>
<Header2 className="mb-2">How to use fetch</Header2>
<Paragraph spacing>
You can use the fetch API to make requests to any API. Or a different request library like
axios if you'd prefer. Again wrapping the request in a Task will make sure it's resumable.
</Paragraph>
<CodeBlock
code={`
client.defineJob({
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
trigger: cronTrigger({
cron: "*/5 * * * *", // every 5 minutes
}),
run: async (payload, io, ctx) => {
//wrap anything in io.runTask so it's resumable and displays in logs
const repo = await io.runTask(
"Get org",
async () => {
//you can use fetch, axios, or any other library to make requests
const response = await fetch('https://api.github.com/orgs/nodejs');
return response.json();
},
//you can add metadata to the task to improve the display in the logs
{ name: "Get org", icon: "github" }
);
},
});
`}
highlightedRanges={[[9, 19]]}
className="mb-4"
/>
{api.examples && api.examples.length > 0 ? (
<>
<Header2 className="mb-2">Example {api.name} code</Header2>
<Paragraph spacing className="mb-4">
This is how you can use {api.name} with Trigger.dev. This code can be copied and
modified to suit your use-case.
</Paragraph>
{api.examples.length > 1 && (
<div className=" flex w-full flex-row gap-4 overflow-x-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
{api.examples?.map((example, index) => (
<button
onClick={() => changeCodeExample(index)}
key={example.codeUrl}
className={cn(
"w-64 min-w-[16rem] p-2 transition-colors duration-300 sm:w-full sm:rounded",
"border-px focus:border-px cursor-pointer border border-slate-900 bg-slate-900 text-slate-300 transition duration-300 hover:bg-slate-800 focus:border focus:border-indigo-600"
)}
>
{example.title}
</button>
))}
</div>
)}
<CodeExample example={api.examples[selectedExample]} />
</>
) : (
<>
<Header2 className="mb-2">Getting started with {api.name}</Header2>
<Paragraph spacing className="mb-4">
We recommend searching for the official {api.name} Node SDK. If they have one, you can
install it and then use their API documentation to get started and create tasks. If they
don't, there are often third party SDKs you can use instead.
</Paragraph>
<Paragraph spacing className="mb-4">
Please{" "}
<Feedback
button={
<span className="cursor-pointer text-indigo-500 transition duration-300 hover:text-indigo-400">
reach out to us
</span>
}
defaultValue="help"
/>{" "}
if you're having any issues connecting to {api.name}, we'll help you get set up as
quickly as possible.
</Paragraph>
</>
)}
</div>
);
}
@@ -1,14 +1,10 @@
import { useFetcher } from "@remix-run/react";
import React from "react";
import { Api } from "~/services/externalApis/apis";
import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { Api } from "~/services/externalApis/apis.server";
import { Header1 } from "../primitives/Headers";
import { NamedIconInBox } from "../primitives/NamedIcon";
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
import { CustomHelp } from "./CustomHelp";
import { CheckIcon } from "@heroicons/react/24/solid";
import { useFetcher } from "@remix-run/react";
import { Paragraph } from "../primitives/Paragraph";
export function NoIntegrationSheet({
api,
@@ -31,27 +27,9 @@ export function NoIntegrationSheet({
<NamedIconInBox name={api.identifier} className="h-9 w-9" />
<Header1>{api.name}</Header1>
</div>
{requested ? (
<div className="flex items-center gap-1">
<CheckIcon className="h-4 w-4 text-green-500" />
<Paragraph variant="small">
We'll let you know when the Integration is available.
</Paragraph>
</div>
) : (
<fetcher.Form method="post" action={`/resources/apivote/${api.identifier}`}>
<Button
variant="primary/small"
disabled={isLoading}
LeadingIcon={isLoading ? "spinner-white" : undefined}
>
{isLoading ? "Saving…" : `I want an Integration for ${api.name}`}
</Button>
</fetcher.Form>
)}
</SheetHeader>
<SheetBody>
<CustomHelp name={api.name} />
<CustomHelp api={api} />
</SheetBody>
</SheetContent>
</Sheet>
@@ -45,7 +45,8 @@ export function UpdateOAuthForm({
const { isManagedCloud } = useFeatures();
const [form, { title, scopes, hasCustomClient, customClientId, customClientSecret }] = useForm({
lastSubmission: fetcher.data,
// TODO: type this
lastSubmission: fetcher.data as any,
onValidate({ formData }) {
return parse(formData, {
schema,
@@ -116,7 +117,7 @@ export function UpdateOAuthForm({
id="hasCustomClient"
label="Use my OAuth App"
variant="simple/small"
disabled={requiresCustomOAuthApp}
readOnly={requiresCustomOAuthApp}
onChange={(checked) => setUseMyOAuthApp(checked)}
{...conform.input(hasCustomClient, { type: "checkbox" })}
defaultChecked={requiresCustomOAuthApp}
@@ -1,20 +1,45 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { useFetcher } from "@remix-run/react";
import { useEffect } from "react";
import { loader } from "~/routes/resources.jobs.$jobId";
import { cn } from "~/utils/cn";
import { JobStatusTable } from "../JobsStatusTable";
import { JobEnvironment, JobStatusTable } from "../JobsStatusTable";
import { Button } from "../primitives/Buttons";
import { Header1, Header2 } from "../primitives/Headers";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import { TextLink } from "../primitives/TextLink";
import { useFetcher } from "@remix-run/react";
import { Spinner } from "../primitives/Spinner";
import { TextLink } from "../primitives/TextLink";
import { useTypedFetcher } from "remix-typedjson";
type JobEnvironment = {
type: RuntimeEnvironmentType;
lastRun?: Date;
version: string;
enabled: boolean;
};
export function DeleteJobDialog({ id, title, slug }: { id: string; title: string; slug: string }) {
const fetcher = useTypedFetcher<typeof loader>();
useEffect(() => {
fetcher.load(`/resources/jobs/${id}`);
}, [id]);
const isLoading = fetcher.state === "loading" || fetcher.state === "submitting";
if (isLoading || !fetcher.data) {
return (
<div className="flex w-full flex-col items-center gap-y-6">
<div className="mt-5 flex flex-col items-center justify-center gap-y-2">
<Header1>{title}</Header1>
<Paragraph variant="small">ID: {slug}</Paragraph>
</div>
<Spinner />
</div>
);
} else {
return (
<DeleteJobDialogContent
id={id}
title={title}
slug={slug}
environments={fetcher.data.environments}
/>
);
}
}
type DeleteJobDialogContentProps = {
id: string;
@@ -40,7 +65,7 @@ export function DeleteJobDialogContent({
return (
<div className="flex w-full flex-col items-center gap-y-6">
<div className="flex flex-col items-center justify-center gap-y-2">
<div className="mt-5 flex flex-col items-center justify-center gap-y-2">
<Header1>{title}</Header1>
<Paragraph variant="small">ID: {slug}</Paragraph>
</div>
+9 -16
View File
@@ -1,7 +1,7 @@
import { ProjectJob } from "~/hooks/useJobs";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { JobRunStatus } from "~/models/job.server";
import { ProjectJob } from "~/presenters/JobListPresenter.server";
import { jobPath, jobTestPath } from "~/utils/pathBuilder";
import { Button } from "../primitives/Buttons";
import { DateTime } from "../primitives/DateTime";
@@ -22,15 +22,14 @@ import {
} from "../primitives/Table";
import { SimpleTooltip } from "../primitives/Tooltip";
import { runStatusTitle } from "../runs/RunStatuses";
import { DeleteJobDialogContent } from "./DeleteJobModalContent";
import { DeleteJobDialog, DeleteJobDialogContent } from "./DeleteJobModalContent";
import { JobStatusBadge } from "./JobStatusBadge";
export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) {
const organization = useOrganization();
const project = useProject();
return (
<Table>
<Table containerClassName="mb-4">
<TableHeader>
<TableRow>
<TableHeaderCell>Job</TableHeaderCell>
@@ -45,7 +44,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
<TableBody>
{jobs.length > 0 ? (
jobs.map((job) => {
const path = jobPath(organization, project, job);
const path = jobPath(organization, { slug: job.projectSlug }, job);
return (
<TableRow key={job.id} className="group">
<TableCell to={path}>
@@ -100,7 +99,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
{job.properties && (
<SimpleTooltip
button={
<div className="flex max-w-[200px] items-start justify-start gap-5 truncate">
<div className="flex max-w-[300px] items-start justify-start gap-5 truncate">
{job.properties.map((property, index) => (
<LabelValueStack
key={index}
@@ -153,26 +152,20 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
<TableCellMenu isSticky>
<PopoverMenuItem to={path} title="View Job" icon="eye" />
<PopoverMenuItem
to={jobTestPath(organization, project, job)}
to={jobTestPath(organization, { slug: job.projectSlug }, job)}
title="Test Job"
icon="beaker"
/>
<Dialog>
<DialogTrigger asChild>
<Button variant="menu-item" LeadingIcon="trash-can">
<Button variant="small-menu-item" LeadingIcon="trash-can" className="text-xs">
Delete Job
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DeleteJobDialogContent
id={job.id}
title={job.title}
slug={job.slug}
environments={job.environments}
/>
</DialogHeader>
<DialogHeader>Delete Job</DialogHeader>
<DeleteJobDialog id={job.id} title={job.title} slug={job.slug} />
</DialogContent>
</Dialog>
</TableCellMenu>
+25 -41
View File
@@ -1,42 +1,8 @@
import { cn } from "~/utils/cn";
import gradientPath from "./app-container-gradient.svg";
/** This container is used to surround the entire app, it correctly places the nav bar */
export function AppContainer({
children,
showBackgroundGradient,
}: {
children: React.ReactNode;
showBackgroundGradient?: boolean;
}) {
return (
<BackgroundGradient showBackgroundGradient={showBackgroundGradient}>
<div className={cn("grid h-full w-full grid-rows-[2.75rem_auto]")}>{children}</div>
</BackgroundGradient>
);
}
export function BackgroundGradient({
children,
showBackgroundGradient,
}: {
children: React.ReactNode;
showBackgroundGradient?: boolean;
}) {
return (
<div
className={cn("h-full w-full bg-contain bg-right-top bg-no-repeat")}
style={
showBackgroundGradient
? {
backgroundImage: `url(${gradientPath})`,
}
: undefined
}
>
{children}
</div>
);
export function AppContainer({ children }: { children: React.ReactNode }) {
return <div className={cn("grid h-full w-full grid-rows-1 overflow-hidden")}>{children}</div>;
}
/** This container should be placed around the content on a page */
@@ -47,7 +13,11 @@ export function PageContainer({
children: React.ReactNode;
className?: string;
}) {
return <div className={cn("grid h-full grid-rows-[auto_1fr]", className)}>{children}</div>;
return (
<div className={cn("grid h-full grid-rows-[auto_1fr] overflow-hidden", className)}>
{children}
</div>
);
}
export function PageBody({
@@ -58,7 +28,15 @@ export function PageBody({
scrollable?: boolean;
}) {
return (
<div className={cn(scrollable ? "overflow-y-auto p-4 " : "overflow-hidden")}>{children}</div>
<div
className={cn(
scrollable
? "overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
: "overflow-hidden"
)}
>
{children}
</div>
);
}
@@ -66,10 +44,16 @@ export function PageBodyPadding({ children }: { children: React.ReactNode }) {
return <div className="p-4">{children}</div>;
}
export function MainCenteredContainer({ children }: { children: React.ReactNode }) {
export function MainCenteredContainer({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div className="h-full w-full">
<div className="mx-auto mt-[30vh] max-w-xs">{children}</div>
<div className="h-full w-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<div className={cn("mx-auto mt-[25vh] max-w-xs overflow-y-auto", className)}>{children}</div>
</div>
);
}
@@ -0,0 +1,104 @@
import { Link } from "@remix-run/react";
import { User } from "@trigger.dev/database";
import { cn } from "~/utils/cn";
import { accountPath, personalAccessTokensPath, rootPath } from "~/utils/pathBuilder";
import { Header3 } from "../primitives/Headers";
import { ArrowLeftIcon, ChevronLeftIcon } from "@heroicons/react/24/solid";
import { SideMenuHeader } from "./SideMenuHeader";
import { SideMenuItem } from "./SideMenuItem";
import { DiscordIcon } from "@trigger.dev/companyicons";
import { Feedback } from "../Feedback";
import { Button, LinkButton } from "../primitives/Buttons";
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
import { useV3Enabled } from "~/root";
export function AccountSideMenu({ user }: { user: User }) {
const v3Enabled = useV3Enabled();
return (
<div
className={cn(
"flex h-full flex-col gap-y-8 overflow-hidden border-r border-ui-border transition"
)}
>
<div className="flex h-full flex-col">
<div
className={cn("flex items-center justify-between border-b bg-background p-px transition")}
>
<LinkButton
variant="tertiary/medium"
LeadingIcon={ArrowLeftIcon}
to={rootPath()}
fullWidth
textAlignLeft
>
Account
</LinkButton>
</div>
<div className="h-full overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<div className="mb-6 flex flex-col gap-1 px-1">
<SideMenuHeader title={user.name ?? user.displayName ?? user.email} />
<SideMenuItem
name="Your profile"
icon="account"
iconColor="text-indigo-500"
to={accountPath()}
data-action="account"
/>
</div>
{v3Enabled && (
<div className="mb-1 flex flex-col gap-1 px-1">
<SideMenuHeader title="Security" />
<SideMenuItem
name="Personal Access Tokens"
icon={ShieldCheckIcon}
iconColor="text-emerald-500"
to={personalAccessTokensPath()}
data-action="tokens"
/>
</div>
)}
</div>
<div className="flex flex-col gap-1 border-t border-border p-1">
<SideMenuItem
name="Join our Discord"
icon={DiscordIcon}
to="https://trigger.dev/discord"
data-action="join our discord"
target="_blank"
/>
<SideMenuItem
name="Documentation"
icon="docs"
to="https://trigger.dev/docs"
data-action="documentation"
target="_blank"
/>
<SideMenuItem
name="Changelog"
icon="star"
to="https://trigger.dev/changelog"
data-action="changelog"
target="_blank"
/>
<Feedback
button={
<Button
variant="small-menu-item"
LeadingIcon="log"
data-action="help & feedback"
fullWidth
textAlignLeft
>
Help & Feedback
</Button>
}
/>
</div>
</div>
</div>
);
}
@@ -1,26 +1,35 @@
import { RouteMatch, useMatches } from "@remix-run/react";
import { UIMatch, useMatches } from "@remix-run/react";
import { Fragment, ReactNode } from "react";
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
import { Handle } from "~/utils/handle";
import { LinkButton } from "../primitives/Buttons";
export type BreadcrumbItem = (match: RouteMatch, allMatches: RouteMatch[]) => ReactNode;
export type BreadcrumbItem = (match: UIMatch, allMatches: UIMatch[]) => ReactNode;
export function Breadcrumb() {
const matches = useMatches();
const matches = useMatches() as UIMatch<unknown, Handle>[];
return (
<div className="hidden items-center md:flex">
{matches.map((match) => {
if (!match.handle || !match.handle.breadcrumb) return null;
<div className="flex items-center px-1">
{matches
.filter((b) => b.handle && b.handle.breadcrumb)
.map((match, index) => {
const breadcrumb = match.handle.breadcrumb as BreadcrumbItem;
const breadcrumb = match.handle.breadcrumb as BreadcrumbItem;
return (
<Fragment key={match.id}>
<BreadcrumbIcon />
{breadcrumb(match, matches)}
</Fragment>
);
})}
return (
<Fragment key={match.id}>
{index !== 0 && <BreadcrumbIcon />} {breadcrumb(match, matches)}
</Fragment>
);
})}
</div>
);
}
export function BreadcrumbLink({ title, to }: { title: string; to: string }) {
return (
<LinkButton to={to} variant="tertiary/small">
{title}
</LinkButton>
);
}
@@ -1,65 +0,0 @@
import { Link, RouteMatch } from "@remix-run/react";
import { useState } from "react";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useJobs } from "~/hooks/useJobs";
import { cn } from "~/utils/cn";
import { jobPath } from "~/utils/pathBuilder";
import { LabelValueStack } from "../primitives/LabelValueStack";
import { NamedIcon } from "../primitives/NamedIcon";
import {
Popover,
PopoverArrowTrigger,
PopoverContent,
PopoverSectionHeader,
} from "../primitives/Popover";
export function JobsMenu({ matches }: { matches: RouteMatch[] }) {
const [isOpen, setIsOpen] = useState(false);
const organization = useOrganization(matches);
const project = useProject(matches);
const projectJobs = useJobs(matches);
const currentJob = useJob(matches);
return (
<>
<Popover onOpenChange={(open) => setIsOpen(open)}>
<PopoverArrowTrigger isOpen={isOpen}>
{currentJob?.title ?? "Select a job"}
</PopoverArrowTrigger>
<PopoverContent
className="min-w-[20rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
align="start"
collisionPadding={20}
>
<PopoverSectionHeader title="Jobs" />
<div className="flex flex-col gap-1 p-1">
{projectJobs.map((job) => {
const isSelected = job.id === currentJob?.id;
return (
<Link
key={job.id}
to={jobPath(organization, project, job)}
className={cn(
"flex w-full items-center gap-2 rounded-md p-2 transition hover:bg-slate-800",
isSelected && "bg-slate-750 group-hover:bg-slate-750"
)}
>
<NamedIcon name={job.event.icon} className="h-8 w-8" />
<LabelValueStack
label={job.title}
value={job.slug}
variant="primary"
className="w-full"
/>
{isSelected && <NamedIcon name="check" className="mr-1 h-6 w-6" />}
</Link>
);
})}
</div>
</PopoverContent>
</Popover>
</>
);
}
@@ -1,128 +0,0 @@
import { Popover, Transition } from "@headlessui/react";
import { BookOpenIcon, ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid";
import { Link } from "@remix-run/react";
import { Fragment } from "react";
import { cn } from "~/utils/cn";
import { Feedback } from "../Feedback";
import { LogoIcon } from "../LogoIcon";
import { Button, LinkButton } from "../primitives/Buttons";
import { Breadcrumb } from "./Breadcrumb";
import { docsRoot } from "~/utils/pathBuilder";
export function NavBar() {
return (
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-uiBorder py-1 pl-1 pr-2.5">
<div className="flex gap-0.5">
<Link to="/" className="p-2">
<LogoIcon className="h-5 w-5" />
</Link>
<Breadcrumb />
<MobileDropdownMenu />
</div>
<div className="hidden items-center gap-2 sm:flex">
<LinkButton to={docsRoot()} variant="secondary/small" LeadingIcon={BookOpenIcon}>
Documentation
</LinkButton>
<Feedback
button={
<Button
variant="secondary/small"
LeadingIcon={ChatBubbleLeftRightIcon}
shortcut={{ key: "f" }}
>
Help & feedback
</Button>
}
/>
</div>
</div>
);
}
export function BreadcrumbLink({ title, to }: { title: string; to: string }) {
return (
<LinkButton to={to} variant="tertiary/small">
{title}
</LinkButton>
);
}
function MobileDropdownMenu() {
return (
<Popover className="block sm:hidden">
<Popover.Button
className="bg-slate-70 relative z-10 flex h-8 w-8 items-center justify-center rounded border-none bg-opacity-50 focus-visible:border-none focus-visible:outline-none"
aria-label="Toggle Navigation"
>
{({ open }) => <MobileNavIcon open={open} />}
</Popover.Button>
<Transition.Root>
<Transition.Child
as={Fragment}
enter="duration-150 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-150 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Overlay className="absolute left-0 top-0 h-full w-full origin-top bg-midnight-900/80" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="duration-150 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Popover.Panel
as="div"
className="absolute inset-x-6 top-0 mt-20 flex origin-top flex-col gap-4 rounded-md bg-slate-800 p-4 text-lg tracking-tight text-slate-900 shadow-xl ring-1 ring-slate-700"
>
{/* <PrimaryA
href={docsRoot()}
target="_blank"
className="max-w-full"
>
<ArrowTopRightOnSquareIcon className="-ml-1 h-4 w-4" />
Documentation
</PrimaryA>
<PrimaryButton
data-attr="posthog-feedback-button"
className="max-w-full"
>
<ChatBubbleLeftRightIcon className="-ml-1 h-4 w-4" />
Send us feedback
</PrimaryButton>
<SecondaryLink to="/logout" className="max-w-full">
Logout
</SecondaryLink> */}
</Popover.Panel>
</Transition.Child>
</Transition.Root>
</Popover>
);
}
function MobileNavIcon({ open }: { open: boolean }) {
return (
<svg
aria-hidden="true"
className="h-3.5 w-3.5 overflow-visible stroke-slate-300"
fill="none"
strokeWidth={2}
strokeLinecap="round"
>
<path
d="M0 1H14M0 7H14M0 13H14"
className={cn("origin-center transition", open && "scale-90 opacity-0")}
/>
<path
d="M2 2L12 12M12 2L2 12"
className={cn("origin-center transition", !open && "scale-90 opacity-0")}
/>
</svg>
);
}
@@ -0,0 +1,10 @@
import { useNavigation } from "@remix-run/react";
import { Spinner } from "../primitives/Spinner";
import { cn } from "~/utils/cn";
export function PageNavigationIndicator({ className }: { className?: string }) {
const navigation = useNavigation();
if (navigation.state === "loading") {
return <Spinner color="blue" className={cn("h-4 w-4", className)} />;
}
}
@@ -1,206 +0,0 @@
import { useMatches } from "@remix-run/react";
import { motion } from "framer-motion";
import { useOptionalJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import {
accountPath,
organizationBillingPath,
organizationTeamPath,
projectEnvironmentsPath,
projectIntegrationsPath,
projectPath,
projectSetupPath,
projectTriggersPath,
} from "~/utils/pathBuilder";
import { UserProfilePhoto } from "../UserProfilePhoto";
import { NavLinkButton } from "../primitives/Buttons";
import { NamedIcon, type IconNames } from "../primitives/NamedIcon";
import { SimpleTooltip } from "../primitives/Tooltip";
export function SideMenuContainer({ children }: { children: React.ReactNode }) {
return <div className="flex h-full w-full justify-stretch overflow-hidden">{children}</div>;
}
const expandedWith = "14rem";
const collapsedWith = "2.81rem";
const menuVariants = {
expanded: {
minWidth: expandedWith,
width: expandedWith,
},
collapsed: {
minWidth: collapsedWith,
width: collapsedWith,
},
};
export function ProjectSideMenu() {
const organization = useOrganization();
const project = useProject();
const matches = useMatches();
//the deepest route `handle` determines if the menu is expanded
const deepestMatch = matches.at(-1);
const handle = deepestMatch?.handle as Handle;
const isCollapsed = handle?.expandSidebar ? !handle.expandSidebar : true;
const job = useOptionalJob();
const jobsActive =
job !== undefined ||
deepestMatch?.id === "routes/_app.orgs.$organizationSlug.projects.$projectParam._index";
return (
<motion.div
animate={isCollapsed ? "collapsed" : "expanded"}
variants={menuVariants}
initial={isCollapsed ? "collapsed" : "expanded"}
transition={{ type: "spring", duration: 0.5 }}
className={cn(
"flex h-full flex-col justify-between overflow-hidden border-r border-uiBorder p-1 transition"
)}
>
<div className="flex flex-col gap-1">
<SideMenuItem
name="Jobs"
icon="job"
to={projectPath(organization, project)}
isCollapsed={isCollapsed}
forceActive={jobsActive}
data-action="jobs"
/>
<SideMenuItem
name="Integrations"
icon="integration"
to={projectIntegrationsPath(organization, project)}
isCollapsed={isCollapsed}
hasWarning={project.hasUnconfiguredIntegrations}
data-action="integrations"
/>
<SideMenuItem
name="Triggers"
icon="trigger"
to={projectTriggersPath(organization, project)}
isCollapsed={isCollapsed}
hasWarning={project.hasInactiveExternalTriggers}
data-action="triggers"
/>
<SideMenuItem
name="Environments & API Keys"
icon="environment"
to={projectEnvironmentsPath(organization, project)}
isCollapsed={isCollapsed}
data-action="environments & api keys"
/>
</div>
<div className="flex flex-col gap-1">
<SideMenuItem
name="Team"
icon="team"
to={organizationTeamPath(organization)}
isCollapsed={isCollapsed}
data-action="team"
/>
<SideMenuItem
name="Usage & Billing"
icon="billing"
to={organizationBillingPath(organization)}
isCollapsed={isCollapsed}
data-action="usage & billing"
/>
<SideMenuItem
name="Onboarding"
icon="clipboard-checked"
to={projectSetupPath(organization, project)}
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}
to={accountPath()}
isCollapsed={isCollapsed}
data-action="account"
/>
</div>
</motion.div>
);
}
const itemVariants = {
expanded: {
opacity: 1,
},
collapsed: {
opacity: 0,
},
};
function SideMenuItem({
icon,
name,
to,
isCollapsed,
forceActive,
hasWarning = false,
target,
}: {
icon: IconNames | React.ComponentType<any>;
name: string;
to: string;
isCollapsed: boolean;
hasWarning?: boolean;
forceActive?: boolean;
target?: string;
}) {
return (
<SimpleTooltip
button={
<NavLinkButton
variant="menu-item"
fullWidth
textAlignLeft
LeadingIcon={icon}
leadingIconClassName="text-dimmed"
to={to}
target={target}
className={({ isActive, isPending }) => {
if (forceActive !== undefined) {
isActive = forceActive;
}
return cn(
"relative",
isActive || isPending
? "bg-slate-800 text-bright group-hover:bg-slate-800"
: "text-dimmed group-hover:bg-slate-850 group-hover:text-bright"
);
}}
>
<motion.span
className="shrink-0 pl-1"
animate={isCollapsed ? "collapsed" : "expanded"}
variants={itemVariants}
initial={isCollapsed ? "collapsed" : "expanded"}
>
{name}
</motion.span>
{hasWarning && <NamedIcon name="error" className="absolute left-1 top-1 h-4 w-4" />}
</NavLinkButton>
}
content={name}
side="right"
hidden={!isCollapsed}
/>
);
}
@@ -1,79 +0,0 @@
import { RouteMatch } from "@remix-run/react";
import { Fragment, useState } from "react";
import simplur from "simplur";
import { Badge } from "~/components/primitives/Badge";
import { useIsNewOrganizationPage, useOrganizations } from "~/hooks/useOrganizations";
import { useOptionalProject } from "~/hooks/useProject";
import { newOrganizationPath, newProjectPath, projectPath } from "~/utils/pathBuilder";
import {
Popover,
PopoverArrowTrigger,
PopoverContent,
PopoverMenuItem,
PopoverSectionHeader,
} from "../primitives/Popover";
export function ProjectsMenu({ matches }: { matches: RouteMatch[] }) {
const [isOpen, setIsOpen] = useState(false);
const organizations = useOrganizations(matches);
const isNewOrgPage = useIsNewOrganizationPage(matches);
const currentProject = useOptionalProject(matches);
if (isNewOrgPage) {
return null;
}
return (
<>
<Popover onOpenChange={(open) => setIsOpen(open)}>
<PopoverArrowTrigger isOpen={isOpen}>
{currentProject?.name ?? "Select a project"}
</PopoverArrowTrigger>
<PopoverContent
className="min-w-[20rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
align="start"
collisionPadding={20}
>
{organizations.map((organization) => (
<Fragment key={organization.id}>
<PopoverSectionHeader title={organization.title} />
<div className="flex flex-col gap-1 p-1 ">
{organization.projects.map((project) => {
const isSelected = project.id === currentProject?.id;
return (
<PopoverMenuItem
key={project.id}
to={projectPath(organization, project)}
title={
<div className="flex w-full items-center justify-between pl-1 text-bright">
<span className="grow truncate text-left">{project.name}</span>
<Badge className="mr-0.5">{simplur`${project._count.jobs} job[|s]`}</Badge>
</div>
}
isSelected={isSelected}
icon="folder"
/>
);
})}
<PopoverMenuItem
to={newProjectPath(organization)}
title="New Project"
isSelected={false}
icon="plus"
/>
</div>
</Fragment>
))}
<div className="border-t border-slate-700 p-1">
<PopoverMenuItem
to={newOrganizationPath()}
title="New Organization"
isSelected={false}
icon="plus"
/>
</div>
</PopoverContent>
</Popover>
</>
);
}
@@ -0,0 +1,441 @@
import {
AcademicCapIcon,
ArrowRightIcon,
ArrowRightOnRectangleIcon,
ChartBarIcon,
CursorArrowRaysIcon,
EllipsisHorizontalIcon,
ShieldCheckIcon,
} from "@heroicons/react/20/solid";
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
import { useNavigation } from "@remix-run/react";
import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
import { Fragment, useEffect, useRef, useState } from "react";
import { useFeatures } from "~/hooks/useFeatures";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { MatchedProject } from "~/hooks/useProject";
import { User } from "~/models/user.server";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { cn } from "~/utils/cn";
import {
accountPath,
inviteTeamMemberPath,
logoutPath,
newOrganizationPath,
newProjectPath,
organizationBillingPath,
organizationIntegrationsPath,
organizationPath,
organizationTeamPath,
personalAccessTokensPath,
projectEnvironmentsPath,
projectEventsPath,
projectHttpEndpointsPath,
projectPath,
projectRunsPath,
projectSetupPath,
projectTriggersPath,
} from "~/utils/pathBuilder";
import { Feedback } from "../Feedback";
import { ImpersonationBanner } from "../ImpersonationBanner";
import { LogoIcon } from "../LogoIcon";
import { StepContentContainer } from "../StepContentContainer";
import { UserProfilePhoto } from "../UserProfilePhoto";
import { FreePlanUsage } from "../billing/FreePlanUsage";
import { Button } from "../primitives/Buttons";
import { ClipboardField } from "../primitives/ClipboardField";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
import { Icon } from "../primitives/Icon";
import { Paragraph } from "../primitives/Paragraph";
import {
Popover,
PopoverArrowTrigger,
PopoverContent,
PopoverCustomTrigger,
PopoverMenuItem,
PopoverSectionHeader,
} from "../primitives/Popover";
import { StepNumber } from "../primitives/StepNumber";
import { MenuCount, SideMenuItem } from "./SideMenuItem";
import { SideMenuHeader } from "./SideMenuHeader";
import { useV3Enabled } from "~/root";
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
type SideMenuProject = Pick<
MatchedProject,
"id" | "name" | "slug" | "hasInactiveExternalTriggers" | "jobCount" | "httpEndpointCount"
>;
type SideMenuProps = {
user: SideMenuUser;
project: SideMenuProject;
organization: MatchedOrganization;
organizations: MatchedOrganization[];
};
export function SideMenu({ user, project, organization, organizations }: SideMenuProps) {
const borderRef = useRef<HTMLDivElement>(null);
const [showHeaderDivider, setShowHeaderDivider] = useState(false);
const { isManagedCloud } = useFeatures();
const currentPlan = useCurrentPlan();
useEffect(() => {
const handleScroll = () => {
if (borderRef.current) {
const shouldShowHeaderDivider = borderRef.current.scrollTop > 1;
if (showHeaderDivider !== shouldShowHeaderDivider) {
setShowHeaderDivider(shouldShowHeaderDivider);
}
}
};
borderRef.current?.addEventListener("scroll", handleScroll);
return () => borderRef.current?.removeEventListener("scroll", handleScroll);
}, [showHeaderDivider]);
return (
<div
className={cn(
"flex h-full flex-col gap-y-8 overflow-hidden border-r border-ui-border transition"
)}
>
<div className="flex h-full flex-col">
<div
className={cn(
"flex items-center justify-between border-b bg-background px-1 py-1 transition",
showHeaderDivider ? " border-border" : "border-transparent"
)}
>
<ProjectSelector
organization={organization}
organizations={organizations}
project={project}
/>
<UserMenu user={user} />
</div>
<div
className="h-full overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
ref={borderRef}
>
<div className="mb-6 flex flex-col gap-1 px-1">
<SideMenuHeader title={project.name || "No project found"}>
<PopoverMenuItem
to={projectSetupPath(organization, project)}
title="Framework setup"
icon="plus"
/>
</SideMenuHeader>
<SideMenuItem
name="Jobs"
icon="job"
iconColor="text-indigo-500"
count={project.jobCount}
to={projectPath(organization, project)}
data-action="jobs"
/>
<SideMenuItem
name="Runs"
icon="runs"
iconColor="text-teal-500"
to={projectRunsPath(organization, project)}
/>
<SideMenuItem
name="Triggers"
icon="trigger"
iconColor="text-amber-500"
to={projectTriggersPath(organization, project)}
data-action="triggers"
hasWarning={project.hasInactiveExternalTriggers}
/>
<SideMenuItem
name="Events"
icon={CursorArrowRaysIcon}
iconColor="text-sky-500"
to={projectEventsPath(organization, project)}
/>
<SideMenuItem
name="HTTP endpoints"
icon="http-endpoint"
iconColor="text-pink-500"
count={project.httpEndpointCount}
to={projectHttpEndpointsPath(organization, project)}
data-action="httpendpoints"
/>
<SideMenuItem
name="Environments & API Keys"
icon="environment"
iconColor="text-rose-500"
to={projectEnvironmentsPath(organization, project)}
data-action="environments & api keys"
/>
</div>
<div className="mb-1 flex flex-col gap-1 px-1">
<SideMenuHeader title={organization.title}>
<PopoverMenuItem to={newProjectPath(organization)} title="New Project" icon="plus" />
<PopoverMenuItem
to={inviteTeamMemberPath(organization)}
title="Invite team member"
icon={UserPlusIcon}
leadingIconClassName="text-indigo-500"
/>
</SideMenuHeader>
<SideMenuItem
name="Integrations"
icon="integration"
to={organizationIntegrationsPath(organization)}
data-action="integrations"
hasWarning={organization.hasUnconfiguredIntegrations}
/>
<SideMenuItem
name="Projects"
icon="folder"
to={organizationPath(organization)}
data-action="projects"
/>
<SideMenuItem
name="Team"
icon={UserGroupIcon}
to={organizationTeamPath(organization)}
iconColor="text-sky-500"
data-action="team"
/>
<SideMenuItem
name={isManagedCloud ? "Usage & Billing" : "Usage"}
icon={ChartBarIcon}
to={organizationBillingPath(organization)}
iconColor="text-green-600"
data-action="usage & billing"
/>
</div>
</div>
<div className="flex flex-col gap-1 border-t border-border p-1">
{currentPlan?.subscription?.isPaying === true ? (
<Dialog>
<DialogTrigger asChild>
<Button
variant="small-menu-item"
LeadingIcon={SlackIcon}
data-action="join our slack"
fullWidth
textAlignLeft
>
Join our Slack
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>Join our Slack</DialogHeader>
<div className="mt-2 flex flex-col gap-4">
<div className="flex items-center gap-4">
<Icon icon={SlackIcon} className="h-10 w-10 min-w-[2.5rem]" />
<Paragraph variant="base/bright">
As a subscriber, you have access to a dedicated Slack channel for 1-to-1
support with the Trigger.dev team.
</Paragraph>
</div>
<hr className="border-slate-800" />
<div>
<StepNumber stepNumber="1" title="Create a new Slack channel" />
<StepContentContainer>
<Paragraph>
In your Slack app, create a new channel from the main menu by going to File{" "}
<ArrowRightIcon className="inline h-4 w-4 text-dimmed" /> New Channel
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Setup your channel" />
<StepContentContainer>
<Paragraph>
Name your channel, set its visibility and click 'Create'.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="3" title="Invite Trigger.dev" />
<StepContentContainer>
<Paragraph>
Invite this email address to your channel:{" "}
<ClipboardField
variant="primary/medium"
value="james@trigger.dev"
className="my-2"
/>
</Paragraph>
<Paragraph>
As soon as we can, we'll accept your invitation and say hello!
</Paragraph>
</StepContentContainer>
</div>
</div>
</DialogContent>
</Dialog>
) : (
<SideMenuItem
name="Join our Discord"
icon={DiscordIcon}
to="https://trigger.dev/discord"
data-action="join our discord"
target="_blank"
/>
)}
<SideMenuItem
name="Documentation"
icon="docs"
to="https://trigger.dev/docs"
data-action="documentation"
target="_blank"
/>
<SideMenuItem
name="Changelog"
icon="star"
to="https://trigger.dev/changelog"
data-action="changelog"
target="_blank"
/>
<Feedback
button={
<Button
variant="small-menu-item"
LeadingIcon="log"
data-action="help & feedback"
fullWidth
textAlignLeft
>
Help & Feedback
</Button>
}
/>
{currentPlan && !currentPlan.subscription?.isPaying && currentPlan.usage.runCountCap && (
<FreePlanUsage
to={organizationBillingPath(organization)}
percentage={currentPlan.usage.currentRunCount / currentPlan.usage.runCountCap}
/>
)}
</div>
</div>
</div>
);
}
function ProjectSelector({
project,
organization,
organizations,
}: {
project: SideMenuProject;
organization: MatchedOrganization;
organizations: MatchedOrganization[];
}) {
const [isOrgMenuOpen, setOrgMenuOpen] = useState(false);
const navigation = useNavigation();
useEffect(() => {
setOrgMenuOpen(false);
}, [navigation.location?.pathname]);
return (
<Popover onOpenChange={(open) => setOrgMenuOpen(open)} open={isOrgMenuOpen}>
<PopoverArrowTrigger
isOpen={isOrgMenuOpen}
overflowHidden
className="h-7 w-full justify-between overflow-hidden py-1 pl-2"
>
<LogoIcon className="relative -top-px mr-2 h-4 w-4 min-w-[1rem]" />
<span className="truncate">{organization.title ?? "Select an organization"}</span>
</PopoverArrowTrigger>
<PopoverContent
className="min-w-[16rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
align="start"
style={{ maxHeight: `calc(var(--radix-popover-content-available-height) - 10vh)` }}
>
{organizations.map((organization) => (
<Fragment key={organization.id}>
<PopoverSectionHeader title={organization.title} />
<div className="flex flex-col gap-1 p-1">
{organization.projects.map((p) => {
const isSelected = p.id === project.id;
return (
<PopoverMenuItem
key={p.id}
to={projectPath(organization, p)}
title={
<div className="flex w-full items-center justify-between text-bright">
<span className="grow truncate text-left">{p.name}</span>
<MenuCount count={p.jobCount} />
</div>
}
isSelected={isSelected}
icon="folder"
/>
);
})}
</div>
</Fragment>
))}
<div className="border-t border-slate-800 p-1">
<PopoverMenuItem to={newOrganizationPath()} title="New Organization" icon="plus" />
</div>
</PopoverContent>
</Popover>
);
}
function UserMenu({ user }: { user: SideMenuUser }) {
const [isProfileMenuOpen, setProfileMenuOpen] = useState(false);
const navigation = useNavigation();
const v3Enabled = useV3Enabled();
useEffect(() => {
setProfileMenuOpen(false);
}, [navigation.location?.pathname]);
return (
<Popover onOpenChange={(open) => setProfileMenuOpen(open)}>
<PopoverCustomTrigger isOpen={isProfileMenuOpen} className="p-1">
<UserProfilePhoto
className={cn(
"h-5 w-5 text-slate-600",
user.isImpersonating && "rounded-full border border-yellow-500"
)}
/>
</PopoverCustomTrigger>
<PopoverContent
className="min-w-[12rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
align="start"
>
<Fragment>
<PopoverSectionHeader title={user.email} variant="extra-small" />
<div className="flex flex-col gap-1 p-1">
{user.isImpersonating && <ImpersonationBanner />}
{user.admin && (
<PopoverMenuItem
to={"/admin"}
title="Admin"
icon={AcademicCapIcon}
leadingIconClassName="text-yellow-500"
/>
)}
<PopoverMenuItem
to={accountPath()}
title="View profile"
icon={UserProfilePhoto}
leadingIconClassName="text-indigo-500"
/>
{v3Enabled && (
<PopoverMenuItem
to={personalAccessTokensPath()}
title="Personal Access Tokens"
icon={ShieldCheckIcon}
leadingIconClassName="text-emerald-500"
/>
)}
<PopoverMenuItem
to={logoutPath()}
title="Log out"
icon={ArrowRightOnRectangleIcon}
leadingIconClassName="text-rose-500"
/>
</div>
</Fragment>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,38 @@
import { useNavigation } from "@remix-run/react";
import { useEffect, useState } from "react";
import { Paragraph } from "../primitives/Paragraph";
import { Popover, PopoverContent, PopoverCustomTrigger } from "../primitives/Popover";
import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid";
export function SideMenuHeader({ title, children }: { title: string; children?: React.ReactNode }) {
const [isHeaderMenuOpen, setHeaderMenuOpen] = useState(false);
const navigation = useNavigation();
useEffect(() => {
setHeaderMenuOpen(false);
}, [navigation.location?.pathname]);
return (
<div className="group flex items-center justify-between pl-1.5">
<Paragraph
variant="extra-extra-small/caps"
className="cursor-default truncate text-slate-500"
>
{title}
</Paragraph>
{children !== undefined ? (
<Popover onOpenChange={(open) => setHeaderMenuOpen(open)} open={isHeaderMenuOpen}>
<PopoverCustomTrigger className="p-1">
<EllipsisHorizontalIcon className="h-4 w-4 text-slate-500 transition group-hover:text-bright" />
</PopoverCustomTrigger>
<PopoverContent
className="min-w-max overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
align="start"
>
<div className="flex flex-col gap-1 p-1">{children}</div>
</PopoverContent>
</Popover>
) : null}
</div>
);
}
@@ -0,0 +1,73 @@
import { AnchorHTMLAttributes } from "react";
import { usePathName } from "~/hooks/usePathName";
import { cn } from "~/utils/cn";
import { LinkButton } from "../primitives/Buttons";
import { IconNames } from "../primitives/NamedIcon";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
import { Icon } from "../primitives/Icon";
import { IconExclamationCircle } from "@tabler/icons-react";
export function SideMenuItem({
icon,
iconColor,
name,
to,
hasWarning,
count,
target,
subItem = false,
}: {
icon?: IconNames | React.ComponentType<any>;
iconColor?: string;
name: string;
to: string;
hasWarning?: string | boolean;
count?: number;
target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
subItem?: boolean;
}) {
const pathName = usePathName();
const isActive = pathName === to;
return (
<LinkButton
variant={subItem ? "small-menu-sub-item" : "small-menu-item"}
fullWidth
textAlignLeft
LeadingIcon={icon}
leadingIconClassName={isActive ? iconColor : "text-dimmed"}
to={to}
target={target}
className={cn(
"text-bright group-hover:bg-slate-850",
subItem ? "text-dimmed" : "",
isActive ? "bg-slate-850 text-bright" : "group-hover:text-bright"
)}
>
<div className="flex w-full items-center justify-between">
{name}
<div className="flex items-center gap-1">
{count !== undefined && count > 0 && <MenuCount count={count} />}
{typeof hasWarning === "string" ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
</TooltipTrigger>
<TooltipContent className="flex items-center gap-1 border border-rose-500 bg-rose-500/20 backdrop-blur-xl">
{hasWarning}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
hasWarning && <Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
)}
</div>
</div>
</LinkButton>
);
}
export function MenuCount({ count }: { count: number | string }) {
return <div className="rounded-full bg-slate-900 px-2 py-1 text-xxs text-dimmed">{count}</div>;
}
@@ -7,99 +7,156 @@ import { ShortcutKey } from "./ShortcutKey";
const variant = {
"primary/small": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80",
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80 px-1",
button:
"h-6 px-[5px] text-xs bg-indigo-600 group-hover:bg-indigo-500/90 group-disabled:opacity-50 group-disabled:pointer-events-none",
icon: "h-3.5",
iconSpacing: "gap-x-0.5",
shortcutVariant: "small" as const,
shortcut:
"ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60 justify-self-center",
},
"secondary/small": {
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80 px-1",
button:
"h-6 px-[5px] text-xs bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50 group-disabled:pointer-events-none",
icon: "h-3.5",
iconSpacing: "gap-x-0.5",
shortcutVariant: "small" as const,
shortcut:
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
},
"tertiary/small": {
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80 px-1",
button:
"h-6 px-[5px] text-xs bg-transparent group-hover:bg-slate-850 disabled:opacity-50 group-disabled:pointer-events-none",
icon: "h-3.5",
iconSpacing: "gap-x-0.5",
shortcutVariant: "small" as const,
shortcut:
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
},
"danger/small": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80",
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80 px-1",
button:
"h-6 px-[5px] text-xs bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50 group-disabled:pointer-events-none",
icon: "h-3.5",
iconSpacing: "gap-x-0.5",
shortcutVariant: "small" as const,
shortcut: "ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"primary/medium": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80",
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80 px-1",
button: "h-8 px-2 text-sm bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
icon: "h-4",
iconSpacing: "gap-x-0.5",
shortcutVariant: "medium" as const,
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"secondary/medium": {
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80 px-1",
button: "h-8 px-2 text-sm bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
icon: "h-4",
iconSpacing: "gap-x-0.5",
shortcutVariant: "medium" as const,
shortcut:
"ml-1.5 -mr-0.5 border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright",
},
"tertiary/medium": {
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80 px-1",
button: "h-8 px-2 text-sm bg-transparent group-hover:bg-slate-850 disabled:opacity-50",
icon: "h-4",
iconSpacing: "gap-x-0.5",
shortcutVariant: "medium" as const,
shortcut:
"ml-1.5 -mr-0.5 border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright",
},
"danger/medium": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80",
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80 px-1",
button: "h-8 px-2 text-sm bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50",
icon: "h-4",
iconSpacing: "gap-x-0.5",
shortcutVariant: "medium" as const,
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"primary/large": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-dimmed/80",
textColor: "text-bright group-hover:text-white transition group-disabled:text-dimmed/80 px-1",
button:
"h-10 px-2 text-sm font-medium bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
"h-10 px-2 text-sm font-medium bg-indigo-600 group-hover:bg-indigo-500/90 group-disabled:opacity-50 group-disabled:group-hover:bg-indigo-600",
icon: "h-5",
iconSpacing: "gap-x-0.5",
shortcutVariant: undefined,
shortcut: undefined,
},
"secondary/large": {
textColor: "text-dimmed",
textColor: "text-bright group-disabled:text-dimmed px-1",
button:
"h-10 px-2 text-sm text-dimmed group-hover:text-bright transition font-medium bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
"h-10 px-2 text-sm transition font-medium bg-slate-800 group-hover:bg-slate-700/70 group-disabled:opacity-40 group-disabled:group-hover:bg-slate-800",
icon: "h-5",
iconSpacing: "gap-x-0.5",
shortcutVariant: undefined,
shortcut: undefined,
},
"danger/large": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50",
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50 px-1",
button:
"h-10 px-2 text-md bg-rose-600 group-hover:bg-rose-500 group-disabled:opacity-50 group-disabled:group-hover:bg-rose-600",
icon: "h-5",
iconSpacing: "gap-x-0.5",
shortcutVariant: "medium" as const,
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"primary/extra-large": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-dimmed/80",
button:
"h-12 px-2 text-md font-medium bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
icon: "h-5",
iconSpacing: undefined,
shortcutVariant: undefined,
shortcut: undefined,
},
"secondary/extra-large": {
textColor: "text-dimmed",
button:
"h-12 px-2 text-md text-dimmed group-hover:text-bright transition font-medium bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
icon: "h-5",
iconSpacing: undefined,
shortcutVariant: undefined,
shortcut: undefined,
},
"danger/extra-large": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50",
button:
"h-12 px-2 text-md bg-rose-600 group-hover:bg-rose-500 group-disabled:opacity-50 group-disabled:group-hover:bg-rose-600",
icon: "h-5",
iconSpacing: undefined,
shortcutVariant: "medium" as const,
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"menu-item": {
textColor: "text-bright",
textColor: "text-bright px-1",
button:
"h-9 px-[0.475rem] text-sm rounded-sm bg-transparent group-hover:bg-slate-800 transition",
icon: "h-5",
iconSpacing: "gap-x-0.5",
shortcutVariant: undefined,
shortcut: undefined,
},
"small-menu-item": {
textColor: "text-bright",
button:
"h-[1.8rem] px-[0.4rem] text-2sm rounded-sm text-dimmed bg-transparent group-hover:bg-slate-850 transition",
icon: "h-4",
iconSpacing: "gap-x-1.5",
shortcutVariant: undefined,
shortcut: undefined,
},
"small-menu-sub-item": {
textColor: "text-dimmed",
button:
"h-[1.8rem] px-[0.5rem] ml-5 text-2sm rounded-sm text-dimmed bg-transparent group-hover:bg-slate-850 transition",
icon: undefined,
iconSpacing: undefined,
shortcutVariant: undefined,
shortcut: undefined,
},
@@ -110,7 +167,7 @@ const allVariants = {
variant: variant,
};
type ButtonContentPropsType = {
export type ButtonContentPropsType = {
children?: React.ReactNode;
LeadingIcon?: React.ComponentType<any> | IconNamesOrString;
TrailingIcon?: React.ComponentType<any> | IconNamesOrString;
@@ -140,6 +197,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
// Based on the size prop, we'll use the corresponding variant classnames
const btnClassName = cn(allVariants.$all, variation.button);
const iconClassName = variation.icon;
const iconSpacingClassName = variation.iconSpacing;
const shortcutClassName = variation.shortcut;
const textColorClassName = variation.textColor;
@@ -148,7 +206,8 @@ export function ButtonContent(props: ButtonContentPropsType) {
<div
className={cn(
textAlignLeft ? "text-left" : "justify-center",
"flex w-full items-center gap-x-0.5"
"flex w-full items-center",
iconSpacingClassName
)}
>
{LeadingIcon &&
@@ -170,7 +229,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
{text &&
(typeof text === "string" ? (
<span className={cn("mx-auto grow self-center truncate px-1", textColorClassName)}>
<span className={cn("mx-auto grow self-center truncate", textColorClassName)}>
{text}
</span>
) : (
@@ -245,8 +304,9 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
}
);
type LinkPropsType = Pick<LinkProps, "to" | "target"> & React.ComponentProps<typeof ButtonContent>;
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
type LinkPropsType = Pick<LinkProps, "to" | "target" | "onClick"> &
React.ComponentProps<typeof ButtonContent>;
export const LinkButton = ({ to, onClick, ...props }: LinkPropsType) => {
const innerRef = useRef<HTMLAnchorElement>(null);
if (props.shortcut) {
useShortcutKeys({
@@ -265,6 +325,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
href={to.toString()}
ref={innerRef}
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
onClick={onClick}
>
<ButtonContent {...props} />
</ExtLink>
@@ -275,6 +336,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
to={to}
ref={innerRef}
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
onClick={onClick}
>
<ButtonContent {...props} />
</Link>
@@ -7,12 +7,14 @@ import {
import {
ArrowTopRightOnSquareIcon,
BookOpenIcon,
ChartBarIcon,
CheckCircleIcon,
ChevronRightIcon,
} from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import { Spinner } from "./Spinner";
export const variantClasses = {
info: {
@@ -51,19 +53,35 @@ 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",
},
pricing: {
className: "border-indigo-400/20 bg-indigo-800/30",
icon: <ChartBarIcon className="h-5 w-5 shrink-0 text-indigo-500" />,
textColor: "text-indigo-200",
linkClassName: "transition hover:bg-indigo-400/40",
},
} as const;
export type CalloutVariant = keyof typeof variantClasses;
export function Callout({
children,
className,
icon,
cta,
variant,
to,
}: {
children?: React.ReactNode;
className?: string;
icon?: React.ReactNode;
variant: keyof typeof variantClasses;
cta?: React.ReactNode;
variant: CalloutVariant;
to?: string;
}) {
const variantDefinition = variantClasses[variant];
@@ -75,13 +93,13 @@ export function Callout({
href={to}
target="_blank"
className={cn(
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
`flex w-full items-center justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
variantDefinition.className,
variantDefinition.linkClassName,
className
)}
>
<div className={"flex w-full items-start gap-x-2"}>
<div className={"flex w-full items-center gap-x-2"}>
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
@@ -126,20 +144,24 @@ export function Callout({
return (
<div
className={cn(
`flex w-full items-start gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
"flex w-full items-center gap-2 rounded-md border pl-2 pr-2 shadow-md backdrop-blur-sm",
cta ? "py-2" : "py-2.5",
variantDefinition.className,
className
)}
>
{icon ? icon : variantDefinition.icon}
<div className={cn(`flex w-full items-center gap-2.5`)}>
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.textColor}>
{children}
</Paragraph>
) : (
children
)}
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.textColor}>
{children}
</Paragraph>
) : (
children
)}
</div>
{cta && cta}
</div>
);
}
@@ -109,14 +109,16 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
return (
<div
className={cn(
"group flex cursor-pointer items-start gap-x-2 transition",
"group flex items-start gap-x-2 transition ",
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
buttonClassName,
isChecked && isCheckedClassName,
isDisabled && isDisabledClassName,
(isDisabled || props.readOnly) && isDisabledClassName,
className
)}
onClick={(e) => {
if (isDisabled) return;
//returning false is not setting the state to false, it stops the event from bubbling up
if (isDisabled || props.readOnly === true) return false;
setIsChecked((c) => !c);
}}
>
@@ -127,12 +129,15 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
value={value}
checked={isChecked}
onChange={(e) => {
//returning false is not setting the state to false, it stops the event from bubbling up
if (isDisabled || props.readOnly === true) return false;
setIsChecked(!isChecked);
}}
disabled={isDisabled}
className={cn(
inputPositionClasses,
"cursor-pointer rounded-sm border border-slate-700 bg-transparent transition checked:!bg-indigo-500 group-hover:bg-slate-900 group-hover:checked:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-none focus-visible:ring-indigo-500 disabled:border-slate-650 disabled:!bg-slate-700"
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
"rounded-sm border border-slate-700 bg-transparent transition checked:!bg-indigo-500 read-only:border-slate-650 read-only:!bg-slate-700 group-hover:bg-slate-900 group-hover:checked:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-none focus-visible:ring-indigo-500 disabled:border-slate-650 disabled:!bg-slate-700"
)}
id={id}
ref={ref}
@@ -141,7 +146,10 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
<div className="flex items-center gap-x-2">
<label
htmlFor={id}
className={cn("cursor-pointer", labelClassName)}
className={cn(
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
labelClassName
)}
onClick={(e) => e.preventDefault()}
>
{label}
@@ -3,6 +3,7 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "~/utils/cn";
import { motion } from "framer-motion";
const ClientTabs = TabsPrimitive.Root;
@@ -48,4 +49,47 @@ const ClientTabsContent = React.forwardRef<
));
ClientTabsContent.displayName = TabsPrimitive.Content.displayName;
export type TabsProps = {
tabs: {
label: string;
value: string;
}[];
currentValue: string;
className?: string;
layoutId: string;
};
export function ClientTabsWithUnderline({ className, tabs, currentValue, layoutId }: TabsProps) {
return (
<TabsPrimitive.List
className={cn(`flex flex-row gap-x-6 border-b border-slate-700`, className)}
>
{tabs.map((tab, index) => {
const isActive = currentValue === tab.value;
return (
<TabsPrimitive.Trigger
key={tab.value}
value={tab.value}
className={cn(`group flex flex-col items-center`, className)}
>
<span
className={cn(
"text-sm transition duration-200",
isActive ? "text-indigo-500" : "text-slate-200"
)}
>
{tab.label}
</span>
{isActive ? (
<motion.div layoutId={layoutId} className="mt-1 h-0.5 w-full bg-indigo-500" />
) : (
<div className="mt-1 h-0.5 w-full bg-slate-500 opacity-0 transition duration-200 group-hover:opacity-100" />
)}
</TabsPrimitive.Trigger>
);
})}
</TabsPrimitive.List>
);
}
export { ClientTabs, ClientTabsList, ClientTabsTrigger, ClientTabsContent };
@@ -0,0 +1,243 @@
import { CalendarDateTime, createCalendar } from "@internationalized/date";
import { useDateField, useDateSegment } from "@react-aria/datepicker";
import type { DateFieldState, DateSegment } from "@react-stately/datepicker";
import { useDateFieldState } from "@react-stately/datepicker";
import { Granularity } from "@react-types/datepicker";
import { useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
import { useLocales } from "./LocaleProvider";
import { Button } from "./Buttons";
type DateFieldProps = {
label?: string;
defaultValue?: Date;
minValue?: Date;
maxValue?: Date;
className?: string;
fieldClassName?: string;
granularity: Granularity;
showGuide?: boolean;
showNowButton?: boolean;
showClearButton?: boolean;
onValueChange?: (value: Date | undefined) => void;
};
export function DateField({
label,
defaultValue,
onValueChange,
minValue,
maxValue,
granularity,
className,
fieldClassName,
showGuide = false,
showNowButton = false,
showClearButton = false,
}: DateFieldProps) {
const [value, setValue] = useState<undefined | CalendarDateTime>(
utcDateToCalendarDate(defaultValue)
);
const state = useDateFieldState({
value: value,
onChange: (value) => {
if (value) {
setValue(value);
onValueChange?.(value.toDate("utc"));
}
},
minValue: utcDateToCalendarDate(minValue),
maxValue: utcDateToCalendarDate(maxValue),
shouldForceLeadingZeros: true,
granularity,
locale: "en-US",
createCalendar: (name: string) => {
return createCalendar(name);
},
});
//if the passed in value changes, we should update the date
useEffect(() => {
if (state.value === undefined && defaultValue === undefined) return;
const calendarDate = utcDateToCalendarDate(defaultValue);
//unchanged
if (state.value?.toDate("utc").getTime() === defaultValue?.getTime()) {
return;
}
setValue(calendarDate);
}, [defaultValue]);
const ref = useRef<null | HTMLDivElement>(null);
const { labelProps, fieldProps } = useDateField(
{
label,
},
state,
ref
);
//render if reverse date order
const yearSegment = state.segments.find((s) => s.type === "year")!;
const monthSegment = state.segments.find((s) => s.type === "month")!;
const daySegment = state.segments.find((s) => s.type === "day")!;
const hourSegment = state.segments.find((s) => s.type === "hour")!;
const minuteSegment = state.segments.find((s) => s.type === "minute")!;
const secondSegment = state.segments.find((s) => s.type === "second")!;
const dayPeriodSegment = state.segments.find((s) => s.type === "dayPeriod")!;
return (
<div className={`flex flex-col items-start ${className || ""}`}>
<span {...labelProps} className="mb-1 ml-0.5 text-xs text-slate-300">
{label}
</span>
<div className="flex flex-row items-center gap-1">
<div
{...fieldProps}
ref={ref}
className={cn(
"flex rounded-sm border border-slate-800 bg-midnight-900 p-0.5 px-1.5 transition-colors focus-within:border-slate-500 hover:border-slate-700 focus-within:hover:border-slate-500",
fieldClassName
)}
>
<DateSegment segment={yearSegment} state={state} />
<DateSegment segment={literalSegment("/")} state={state} />
<DateSegment segment={monthSegment} state={state} />
<DateSegment segment={literalSegment("/")} state={state} />
<DateSegment segment={daySegment} state={state} />
<DateSegment segment={literalSegment(", ")} state={state} />
<DateSegment segment={hourSegment} state={state} />
<DateSegment segment={literalSegment(":")} state={state} />
<DateSegment segment={minuteSegment} state={state} />
<DateSegment segment={literalSegment(":")} state={state} />
<DateSegment segment={secondSegment} state={state} />
<DateSegment segment={literalSegment(" ")} state={state} />
<DateSegment segment={dayPeriodSegment} state={state} />
</div>
{showNowButton && (
<Button
variant="secondary/small"
onClick={() => {
const now = new Date();
setValue(utcDateToCalendarDate(new Date()));
onValueChange?.(now);
}}
>
Now
</Button>
)}
{showClearButton && (
<Button
variant="secondary/small"
LeadingIcon={"close"}
onClick={() => {
setValue(undefined);
onValueChange?.(undefined);
state.clearSegment("year");
state.clearSegment("month");
state.clearSegment("day");
state.clearSegment("hour");
state.clearSegment("minute");
state.clearSegment("second");
}}
/>
)}
</div>
{showGuide && (
<div className="mt-1 flex px-2">
{state.segments.map((segment, i) => (
<DateSegmentGuide key={i} segment={segment} />
))}
</div>
)}
</div>
);
}
function utcDateToCalendarDate(date?: Date) {
return date
? new CalendarDateTime(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds()
)
: undefined;
}
type DateSegmentProps = {
segment: DateSegment;
state: DateFieldState;
};
function DateSegment({ segment, state }: DateSegmentProps) {
const ref = useRef<null | HTMLDivElement>(null);
const { segmentProps } = useDateSegment(segment, state, ref);
return (
<div
{...segmentProps}
ref={ref}
style={{
...segmentProps.style,
minWidth: minWidthForSegment(segment),
}}
className={`group box-content rounded-sm px-0.5 text-right text-sm tabular-nums outline-none focus:bg-indigo-500 focus:text-white ${
!segment.isEditable ? "text-slate-500" : "text-bright"
}`}
>
{/* Always reserve space for the placeholder, to prevent layout shift when editing. */}
<span
aria-hidden="true"
className="block text-center italic text-slate-500 group-focus:text-white"
style={{
visibility: segment.isPlaceholder ? undefined : "hidden",
height: segment.isPlaceholder ? "" : 0,
pointerEvents: "none",
}}
>
{segment.placeholder}
</span>
{segment.isPlaceholder ? "" : segment.text}
</div>
);
}
function literalSegment(text: string): DateSegment {
return {
type: "literal",
text,
isPlaceholder: false,
isEditable: false,
placeholder: "",
};
}
function minWidthForSegment(segment: DateSegment) {
if (segment.type === "literal") {
return undefined;
}
return String(`${segment.maxValue}`).length + "ch";
}
function DateSegmentGuide({ segment }: { segment: DateSegment }) {
return (
<div
style={{
minWidth: minWidthForSegment(segment),
}}
className={`group box-content rounded-sm px-0.5 text-right text-sm tabular-nums outline-none ${
!segment.isEditable ? "text-slate-500" : "text-bright"
}`}
>
<span className="block text-center italic text-slate-500">
{segment.type !== "literal" ? segment.placeholder : segment.text}
</span>
</div>
);
}
@@ -5,14 +5,26 @@ type DateTimeProps = {
date: Date | string;
timeZone?: string;
includeSeconds?: boolean;
includeTime?: boolean;
};
export const DateTime = ({ date, timeZone = "UTC", includeSeconds = true }: DateTimeProps) => {
export const DateTime = ({
date,
timeZone = "UTC",
includeSeconds = true,
includeTime = true,
}: DateTimeProps) => {
const locales = useLocales();
const realDate = typeof date === "string" ? new Date(date) : date;
const initialFormattedDateTime = formatDateTime(realDate, timeZone, locales, includeSeconds);
const initialFormattedDateTime = formatDateTime(
realDate,
timeZone,
locales,
includeSeconds,
includeTime
);
const [formattedDateTime, setFormattedDateTime] = useState<string>(initialFormattedDateTime);
@@ -20,26 +32,27 @@ export const DateTime = ({ date, timeZone = "UTC", includeSeconds = true }: Date
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
setFormattedDateTime(
formatDateTime(realDate, resolvedOptions.timeZone, locales, includeSeconds)
formatDateTime(realDate, resolvedOptions.timeZone, locales, includeSeconds, includeTime)
);
}, [locales, includeSeconds, realDate]);
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
};
function formatDateTime(
export function formatDateTime(
date: Date,
timeZone: string,
locales: string[],
includeSeconds: boolean
includeSeconds: boolean,
includeTime: boolean
): string {
return new Intl.DateTimeFormat(locales, {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: includeSeconds ? "numeric" : undefined,
hour: includeTime ? "numeric" : undefined,
minute: includeTime ? "numeric" : undefined,
second: includeTime && includeSeconds ? "numeric" : undefined,
timeZone,
}).format(date);
}
@@ -26,7 +26,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
"fixed inset-0 z-50 bg-background/90 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
className
)}
{...props}
@@ -43,12 +43,12 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-midnight-800 p-6 pt-11 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background px-4 pb-6 pt-3 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
className
)}
{...props}
>
<hr className="-ml-6 w-[calc(100%_+_3rem)]" />
<hr className="absolute left-0 top-12 w-full" />
{children}
<DialogPrimitive.Close className="absolute right-3 top-3 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none">
<div className="flex gap-x-2">
@@ -73,7 +73,7 @@ const DialogContent = React.forwardRef<
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
<div className={cn("flex flex-col text-left font-medium text-dimmed", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
@@ -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">
@@ -1,72 +0,0 @@
import { RadioGroup } from "@headlessui/react";
import { cn } from "~/utils/cn";
type Options = {
label: string;
value: string;
};
type FormSegmentedControlProps = {
name: string;
defaultValue?: string;
options: Options[];
onChange?: (value: string) => void;
};
export default function FormSegmentedControl({
name,
defaultValue,
options,
onChange,
}: FormSegmentedControlProps) {
return (
<div className="flex h-8 w-fit rounded bg-slate-800 p-1">
<RadioGroup
defaultValue={defaultValue ?? options[0].value}
name={name}
onChange={(c: string) => {
if (onChange) {
onChange(c);
}
}}
>
<div className="flex gap-x-1.5">
{options.map((option) => (
<RadioGroup.Option
key={option.value}
value={option.value}
className={({ active, checked }) =>
cn(
"relative flex cursor-pointer rounded-[2px] px-3 py-[0.13rem] shadow-md focus:outline-none",
active
? "focus-visible:ring focus-visible:ring-indigo-500 focus-visible:ring-opacity-60"
: "",
checked
? "bg-slate-700 text-bright"
: "bg-transparent transition hover:bg-slate-750"
)
}
>
{({ checked }) => (
<>
<div className="flex w-full items-center justify-between">
<div className="flex items-center">
<div className="text-sm">
<RadioGroup.Label
as="p"
className={cn("font-normal", checked ? "text-bright" : "text-dimmed")}
>
{option.label}
</RadioGroup.Label>
</div>
</div>
</div>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</div>
);
}
@@ -21,7 +21,7 @@ export function FormTitle({
<div
className={cn(
"mb-4 flex flex-col gap-3 pb-4",
divide ? "border-b border-uiBorder" : "",
divide ? "border-b border-ui-border" : "",
className
)}
>
@@ -2,7 +2,7 @@ import { cn } from "~/utils/cn";
const headerVariants = {
header1: {
text: "font-sans text-base md:text-lg lg:text-xl leading-5 md:leading-6 lg:leading-7 font-semibold",
text: "font-sans text-2xl leading-5 md:leading-6 lg:leading-7 font-semibold",
spacing: "mb-2",
},
header2: {
@@ -88,12 +88,7 @@ export function HelpContent({
)}
</div>
<div
className="grow rounded border border-slate-850 bg-midnight-850 bg-contain bg-left-top bg-no-repeat p-6"
style={{
backgroundImage: `url(${gradientPath})`,
}}
>
<div className="grow rounded border border-slate-850 bg-midnight-850/80 bg-contain bg-left-top bg-no-repeat p-6">
{children}
</div>
</div>
@@ -2,11 +2,12 @@ import * as React from "react";
import { cn } from "~/utils/cn";
import type { IconNamesOrString } from "./NamedIcon";
import { NamedIcon } from "./NamedIcon";
import { Icon, RenderIcon } from "./Icon";
const variants = {
large: {
input:
"px-3 flex h-10 w-full text-bright rounded-md border border-slate-800 bg-slate-850 text-sm ring-offset-background transition file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"px-3 flex h-10 w-full text-bright rounded-[3px] border border-slate-800 bg-slate-850 text-sm ring-offset-background transition file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
iconSize: "h-4 w-4 ml-3",
iconOffset: "pl-[34px]",
@@ -33,7 +34,7 @@ const variants = {
},
tertiary: {
input:
"px-1 flex h-6 w-full text-bright rounded bg-transparent border border-transparent transition hover:border-slate-800 hover:bg-slate-850 focus:border-slate-800 focus:bg-slate-850 text-xs ring-offset-background transition file:border-0 file:bg-transparent file:text-xs file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
"px-1 flex h-6 w-full text-bright rounded bg-transparent border border-transparent hover:border-slate-800 hover:bg-slate-850 focus:border-slate-800 focus:bg-slate-850 text-xs ring-offset-background transition file:border-0 file:bg-transparent file:text-xs file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
iconSize: "h-3 w-3 ml-1.5",
iconOffset: "pl-[21px]",
@@ -44,7 +45,7 @@ const variants = {
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
variant?: keyof typeof variants;
icon?: IconNamesOrString;
icon?: RenderIcon;
shortcut?: string;
fullWidth?: boolean;
};
@@ -59,7 +60,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<div className={cn("relative", fullWidth ? "w-full" : "max-w-max")}>
{icon && (
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center">
<NamedIcon name={icon} className={cn(iconClassName, "text-dimmed")} />
<Icon icon={icon} className={cn(iconClassName, "text-dimmed")} />
</div>
)}
<input
@@ -2,6 +2,7 @@ import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
import { SimpleTooltip } from "./Tooltip";
import { Link } from "@remix-run/react";
const variations = {
primary: {
@@ -45,18 +46,7 @@ export function LabelValueStack({
<Paragraph variant={variation.label}>{label}</Paragraph>
<>
{href ? (
<SimpleTooltip
side="bottom"
button={
<Paragraph variant={variation.value}>
<a href={href} className="underline underline-offset-2" target="_blank">
{value}
<ArrowTopRightOnSquareIcon className="ml-1 inline-block h-4 w-4 text-dimmed" />
</a>
</Paragraph>
}
content={href}
/>
<ValueButton value={value} href={href} variant={variant} />
) : (
<Paragraph variant={variation.value}>{value}</Paragraph>
)}
@@ -64,3 +54,40 @@ export function LabelValueStack({
</div>
);
}
type ValueButtonStackProps = {
value: React.ReactNode;
href: string;
variant?: keyof typeof variations;
};
function ValueButton({ value, href, variant = "secondary" }: ValueButtonStackProps) {
const variation = variations[variant];
const isExternalUrl = href.startsWith("http");
if (!isExternalUrl) {
return (
<Paragraph variant={variation.value}>
<Link to={href} reloadDocument className="underline underline-offset-2">
{value}
</Link>
</Paragraph>
);
}
return (
<SimpleTooltip
side="bottom"
button={
<Paragraph variant={variation.value}>
<a href={href} className="underline underline-offset-2" target="_blank">
{value}
<ArrowTopRightOnSquareIcon className="ml-1 inline-block h-4 w-4 text-dimmed" />
</a>
</Paragraph>
}
content={href}
/>
);
}
@@ -59,15 +59,19 @@ import {
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
import { ActivityIcon, HourglassIcon } from "lucide-react";
import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon";
import { EndpointIcon } from "~/assets/icons/EndpointIcon";
import { ErrorIcon } from "~/assets/icons/ErrorIcon";
import { OneTreeIcon } from "~/assets/icons/OneTreeIcon";
import { RunsIcon } from "~/assets/icons/RunsIcon";
import { SaplingIcon } from "~/assets/icons/SaplingIcon";
import { ScheduleIcon } from "~/assets/icons/ScheduleIcon";
import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon";
import { WebhookIcon } from "~/assets/icons/WebhookIcon";
import { cn } from "~/utils/cn";
import { tablerIcons } from "~/utils/tablerIcons";
import { LogoIcon } from "../LogoIcon";
import { Spinner } from "./Spinner";
import { SaplingIcon } from "~/assets/icons/SaplingIcon";
import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon";
import { OneTreeIcon } from "~/assets/icons/OneTreeIcon";
import tablerSpritePath from "./tabler-sprite.svg";
const icons = {
account: (className: string) => <UserCircleIcon className={cn("text-slate-400", className)} />,
@@ -125,7 +129,9 @@ const icons = {
"invite-member": (className: string) => (
<UserPlusIcon className={cn("text-indigo-500", className)} />
),
job: (className: string) => <WrenchScrewdriverIcon className={cn("text-teal-500", className)} />,
job: (className: string) => (
<WrenchScrewdriverIcon className={cn("text-indigo-500", className)} />
),
key: (className: string) => <KeyIcon className={cn("text-amber-400", className)} />,
lightbulb: (className: string) => <LightBulbIcon className={cn("text-amber-400", className)} />,
"clipboard-checked": (className: string) => (
@@ -181,6 +187,11 @@ const icons = {
<ScheduleIcon className={cn("text-sky-500", className)} />
),
webhook: (className: string) => <WebhookIcon className={cn("text-pink-500", className)} />,
endpoint: (className: string) => <EndpointIcon className={cn("text-blue-500", className)} />,
"http-endpoint": (className: string) => (
<GlobeAltIcon className={cn("text-blue-500", className)} />
),
runs: (className: string) => <RunsIcon className={cn("text-lime-500", className)} />,
};
export type IconNames = keyof typeof icons;
@@ -215,7 +226,15 @@ export function NamedIcon({
);
}
console.log(`Icon ${name} not found`);
if (tablerIcons.has("tabler-" + name)) {
return <TablerIcon name={"tabler-" + name} className={className} />;
} else if (name.startsWith("tabler-") && tablerIcons.has(name)) {
return <TablerIcon name={name} className={className} />;
}
if (name === "supabase-management") {
return <NamedIcon name="supabase" className={className} />;
}
if (fallback) {
return fallback;
@@ -247,3 +266,11 @@ export function NamedIconInBox({
</div>
);
}
export function TablerIcon({ name, className }: { name: string; className?: string }) {
return (
<svg className={cn("stroke-[1.5]", className)}>
<use xlinkHref={`${tablerSpritePath}#${name}`} />
</svg>
);
}
@@ -6,6 +6,9 @@ import { Paragraph } from "./Paragraph";
import { cn } from "~/utils/cn";
import { NamedIcon } from "./NamedIcon";
import { Tabs, TabsProps } from "./Tabs";
import { Icon, RenderIcon } from "./Icon";
import { Button, LinkButton } from "./Buttons";
import { ArrowUpRightIcon } from "@heroicons/react/20/solid";
type WithChildren = {
children: React.ReactNode;
@@ -13,7 +16,7 @@ type WithChildren = {
export function PageHeader({ children, hideBorder }: WithChildren & { hideBorder?: boolean }) {
return (
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-uiBorder pb-4")}>
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-ui-border pb-4")}>
{children}
</div>
);
@@ -24,6 +27,7 @@ export function PageTitleRow({ children }: WithChildren) {
}
type PageTitleProps = {
icon?: RenderIcon;
title: string;
backButton?: {
to: string;
@@ -31,7 +35,7 @@ type PageTitleProps = {
};
};
export function PageTitle({ title, backButton }: PageTitleProps) {
export function PageTitle({ icon, title, backButton }: PageTitleProps) {
return (
<div className="flex items-center gap-2">
{backButton && (
@@ -48,7 +52,10 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
<BreadcrumbIcon className="h-6" />
</div>
)}
<Header1>{title}</Header1>
<Header1 className="flex items-center gap-1">
{icon && <Icon icon={icon} className="h-5 w-5" />}
{title}
</Header1>
</div>
);
}
@@ -89,20 +96,43 @@ export function PageInfoProperty({
icon,
label,
value,
to,
}: {
icon?: string | React.ReactNode;
label?: string;
value: React.ReactNode;
value?: React.ReactNode;
to?: string;
}) {
if (to === undefined) {
return <PageInfoPropertyContent icon={icon} label={label} value={value} />;
}
return (
<LinkButton variant="tertiary/small" to={to} TrailingIcon={ArrowUpRightIcon}>
<PageInfoPropertyContent icon={icon} label={label} value={value} />
</LinkButton>
);
}
function PageInfoPropertyContent({
icon,
label,
value,
}: {
icon?: string | React.ReactNode;
label?: string;
value?: React.ReactNode;
}) {
return (
<div className="flex items-center gap-1">
{icon && typeof icon === "string" ? <NamedIcon name={icon} className="h-4 w-4" /> : icon}
{label && (
<Paragraph variant="extra-small/caps" className="mt-0.5 whitespace-nowrap">
{label}:
{label}
{value !== undefined && ":"}
</Paragraph>
)}
<Paragraph variant="small">{value}</Paragraph>
{value !== undefined && <Paragraph variant="small">{value}</Paragraph>}
</div>
);
}
@@ -51,6 +51,7 @@ const paragraphVariants = {
text: "font-sans text-xxs font-normal text-bright",
spacing: "mb-1",
},
"extra-extra-small/caps": {
text: "font-sans text-xxs uppercase tracking-wider font-normal text-dimmed",
spacing: "mb-1",
@@ -59,6 +60,10 @@ const paragraphVariants = {
text: "font-sans text-xxs uppercase tracking-wider font-normal text-bright",
spacing: "mb-1",
},
"extra-extra-small/dimmed/caps": {
text: "font-sans text-xxs uppercase tracking-wider font-normal text-dimmed",
spacing: "mb-1",
},
};
export type ParagraphVariant = keyof typeof paragraphVariants;
@@ -3,12 +3,11 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import { Paragraph, ParagraphVariant } from "./Paragraph";
import { ChevronDownIcon, EllipsisVerticalIcon } from "@heroicons/react/24/solid";
import { LinkButton } from "./Buttons";
import { ButtonContentPropsType, LinkButton } from "./Buttons";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
@@ -22,19 +21,27 @@ const PopoverContent = React.forwardRef<
sideOffset={sideOffset}
avoidCollisions={true}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 min-w-max rounded-md border border-slate-700 bg-midnight-850 p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
style={{ maxHeight: "var(--radix-popover-content-available-height)" }}
style={{
maxHeight: "var(--radix-popover-content-available-height)",
}}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
function PopoverSectionHeader({ title }: { title: string }) {
function PopoverSectionHeader({
title,
variant = "extra-extra-small/dimmed/caps",
}: {
title: string;
variant?: ParagraphVariant;
}) {
return (
<Paragraph variant="extra-extra-small/bright/caps" className="bg-slate-900 px-2.5 py-2">
<Paragraph variant={variant} className="bg-slate-900 px-2.5 py-1.5">
{title}
</Paragraph>
);
@@ -45,28 +52,33 @@ function PopoverMenuItem({
icon,
title,
isSelected,
variant = { variant: "small-menu-item" },
leadingIconClassName,
}: {
to: string;
icon: string;
icon: string | React.ComponentType<any>;
title: React.ReactNode;
isSelected?: boolean;
variant?: ButtonContentPropsType;
leadingIconClassName?: string;
}) {
return (
<LinkButton
to={to}
variant="menu-item"
variant={variant.variant}
LeadingIcon={icon}
leadingIconClassName={leadingIconClassName}
fullWidth
textAlignLeft
TrailingIcon={isSelected ? "check" : undefined}
className={isSelected ? "bg-slate-750 group-hover:bg-slate-750" : undefined}
className={isSelected ? "bg-slate-750 group-hover:bg-slate-800" : undefined}
>
{title}
</LinkButton>
);
}
function PopoverArrowTrigger({
function PopoverCustomTrigger({
isOpen,
children,
className,
@@ -76,15 +88,50 @@ function PopoverArrowTrigger({
<PopoverTrigger
{...props}
className={cn(
"group flex h-6 items-center gap-1 rounded px-2 text-dimmed transition hover:bg-slate-850 hover:text-bright",
"group flex items-center justify-end gap-1 rounded text-dimmed transition hover:bg-slate-850 hover:text-bright",
className
)}
>
<Paragraph variant="extra-small" className="transition group-hover:text-bright">
{children}
</PopoverTrigger>
);
}
function PopoverArrowTrigger({
isOpen,
children,
fullWidth = false,
overflowHidden = false,
className,
...props
}: {
isOpen?: boolean;
fullWidth?: boolean;
overflowHidden?: boolean;
} & React.ComponentPropsWithoutRef<typeof PopoverTrigger>) {
return (
<PopoverTrigger
{...props}
className={cn(
"group flex h-6 items-center gap-1 rounded px-2 text-dimmed transition hover:bg-slate-850 hover:text-bright",
fullWidth && "w-full justify-between",
className
)}
>
<Paragraph
variant="extra-small"
className={cn(
"flex transition group-hover:text-bright",
overflowHidden && "overflow-hidden"
)}
>
{children}
</Paragraph>
<ChevronDownIcon
className={cn("h-3 w-3 transition group-hover:text-bright", isOpen && "-rotate-180")}
className={cn(
"h-3 w-3 min-w-[0.75rem] text-slate-600 transition group-hover:text-bright",
isOpen && "-rotate-180"
)}
/>
</PopoverTrigger>
);
@@ -113,7 +160,8 @@ export {
PopoverTrigger,
PopoverContent,
PopoverSectionHeader,
PopoverCustomTrigger,
PopoverArrowTrigger,
PopoverMenuItem,
PopoverVerticalEllipseTrigger,
PopoverMenuItem,
};
@@ -0,0 +1,79 @@
import { RadioGroup } from "@headlessui/react";
import { motion } from "framer-motion";
import { cn } from "~/utils/cn";
type Options = {
label: string;
value: string;
};
type SegmentedControlProps = {
name: string;
value?: string;
defaultValue?: string;
options: Options[];
fullWidth?: boolean;
onChange?: (value: string) => void;
};
export default function SegmentedControl({
name,
value,
defaultValue,
options,
fullWidth,
onChange,
}: SegmentedControlProps) {
return (
<div className={cn("flex h-10 rounded bg-slate-850", fullWidth ? "w-full" : "w-fit")}>
<RadioGroup
value={value}
defaultValue={defaultValue ?? options[0].value}
name={name}
onChange={(c: string) => {
if (onChange) {
onChange(c);
}
}}
className="w-full"
>
<div className="flex h-full w-full items-center justify-between gap-x-1 p-1">
{options.map((option) => (
<RadioGroup.Option
key={option.value}
value={option.value}
className={({ active, checked }) =>
cn(
"relative flex h-full grow cursor-pointer text-center font-normal focus:outline-none",
active
? "ring-offset-2 focus-visible:ring focus-visible:ring-indigo-500 focus-visible:ring-opacity-60"
: "",
checked
? "text-bright"
: "rounded-[2px] text-dimmed transition hover:bg-slate-750/50 hover:text-bright"
)
}
>
{({ checked }) => (
<>
<div className="relative flex h-full w-full items-center justify-between px-3 py-[0.13rem]">
<div className="z-10 flex h-full w-full items-center justify-center text-sm">
<RadioGroup.Label as="p">{option.label}</RadioGroup.Label>
</div>
{checked && (
<motion.div
layoutId={`segmented-control-${name}`}
transition={{ duration: 0.4, type: "spring" }}
className="absolute inset-0 rounded-[2px] bg-indigo-600 shadow-md"
></motion.div>
)}
</div>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</div>
);
}

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