Compare commits

...

235 Commits

Author SHA1 Message Date
Matt Aitken 70f9bd0d70 Latest lockfile 2024-02-12 13:11:14 +00:00
github-actions[bot] a1860dbaae chore: Update version for release (#899)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-12 13:00:14 +00:00
Matt Aitken d69e4e712d OpenAI integration: retry 400 status code because OpenAI returns this sometimes for no reason (#898) 2024-02-12 12:48:48 +00:00
Matt Aitken 7fae67c47d Fix/autoyield canceled run (#890)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 3s
🚀 Publish Trigger.dev Docker / units (push) Failing after 16s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 20s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Todo for the fix

* If the run has been canceled during execution, don’t do any further processing of the run
2024-02-09 11:06:19 +00:00
Matt Aitken 9ae0ca64af Fix edge cases with deleting last org/project (#892)
* If you have an org with no projects, it displays in the project dropdown with a “New project” button

* When creating a new org disable the button whilst it’s doing the request

* If an org already had any deleted projects it couldn’t be deleted…

* When selecting the best project, factor in deleted ones

* Don’t show the cancel button when creating a new org if there are no non-deleted projects

* If a project has already been deleted just return
2024-02-09 11:05:28 +00:00
Matt Aitken da69fa0613 Fixes #886. Removed “public” schema from the JobListPresenter query
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 4s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 20s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2024-02-07 16:48:18 +00:00
Matt Aitken 336029b842 Organization renaming, deleting and one-click email unsubscribe (#885)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 4s
🚀 Publish Trigger.dev Docker / units (push) Failing after 10s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Added the org settings page to the sidebar

* Added loading states when renaming/deleting projects

* Don’t show deleted orgs in the app

* The actual db migration file

* The Org settings page with the actions working

* Don’t remove org members, just leave them

* Don’t show invites from orgs that are deleted

* Allow disabling IntegrationConnections

* Don’t refresh IntegrationConnections that are disabled

* Set all the integrations as disabled

* Updated the unsubscribe checkbox text

* Unsubscribe route

* Use the magic link secret, not the encryption key. Also make the error message more vague

* Only members of the org or project can rename them

* Don’t throw an error if the connection can’t be refreshed, return undefined instead
2024-02-07 10:52:50 +00:00
Matt Aitken 29edcd3df9 Rename and delete projects (#880)
🚀 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 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Side menu: selector title changed to project. Changed section headers and made it Slack + Discord

* Added project settings page. Renaming is working

* Added project.deletedAt column

* Deleting projects WIP

* Allow clearing the project from the session

* Don’t load projects that are deleted

* Improved the project selection logic

* Clear the session project id when deleting a project

* Deleting the last project in an org now works. The new project page doesn’t have the sidebar anymore.

* Removed false code comment

* Added the JobRun index back in, using Prisma. Otherwise it tries to remove it on each migration

* Only return environments where the project isn’t deleted. This will block API calls

* Do the project deletedAt check in code, not SQL

* Made it clearer what the code does with a comment and applied the same logic to the public api key
2024-02-05 13:14:33 +00:00
Matt Aitken a9ff32418e Fix for when there are no jobs…
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 4s
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2024-01-30 17:35:04 +00:00
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
918 changed files with 56841 additions and 9646 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=
+1
View File
@@ -7,6 +7,7 @@ jobs:
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 }}
+2 -1
View File
@@ -1,11 +1,12 @@
name: 🤖 PR Checks
on:
pull_request_target:
pull_request:
branches:
- main
paths-ignore:
- "**.md"
- "**.mdx"
- ".github/CODEOWNERS"
- ".github/ISSUE_TEMPLATE/**"
+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
+3 -69
View File
@@ -4,9 +4,9 @@ on:
push:
branches:
- main
- improvements/*
tags:
- "v.docker.*"
- "build-*"
paths:
- ".github/workflows/publish.yml"
- "packages/**"
@@ -51,71 +51,5 @@ jobs:
publish:
needs: [typecheck, units, 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
uses: ./.github/workflows/publish-docker.yml
secrets: inherit
+10
View File
@@ -22,6 +22,16 @@ jobs:
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
+2
View File
@@ -52,3 +52,5 @@ apps/**/public/build
/test-results/
/playwright-report/
/playwright/.cache/
.cosine
+2 -1
View File
@@ -1,3 +1,4 @@
{
"deno.enablePaths": ["references/deno-reference"]
"deno.enablePaths": ["references/deno-reference", "runtime_tests/tests/deno"],
"debug.toolBarLocation": "commandCenter"
}
-12
View File
@@ -14,18 +14,6 @@
</div>
# ✨🎃 Get involved with Hacktoberfest 2023! 🎃✨
All of October we're participating in Hacktoberfest and invite you to join us! We have a bunch of issues labeled `🎃 Hacktoberfest` that are ready for you to work on which will count towards Hacktoberfest. We are also running our own game, earn 💎 points to win swag!
- Check out our [Hacktoberfest landing page](https://trigger.dev/hacktoberfest) for how to participate and win swag.
- Contribute to either our [/trigger.dev](https://github.com/triggerdotdev/trigger.dev/labels/%F0%9F%8E%83%20hacktoberfest), [/api-reference](https://github.com/triggerdotdev/api-reference/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%8E%83hacktoberfest) or [/jobs-showcase](https://github.com/triggerdotdev/jobs-showcase/labels/%F0%9F%8E%83%20hacktoberfest) repositories and complete issues marked `🎃 Hacktoberfest` to be eligible for swag.
- Join our [Discord](https://discord.gg/JtBAxBr2m3) and get involved with the community.
_New to Hacktober? Check out the [Hacktoberfest website](https://hacktoberfest.digitalocean.com/) for more information._
🎃 **Happy Hacking!** 🎃
# About Trigger.dev
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
+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]
+3
View File
@@ -23,12 +23,15 @@ const preview: Preview = {
},
],
},
layout: "fullscreen",
},
decorators: [
(Story) => {
const RemixStub = createRemixStub([
{
path: "/*",
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>
);
}
+24 -23
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;
@@ -55,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">
@@ -90,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>
@@ -109,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"
>
@@ -120,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>
);
}
@@ -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>
);
}
@@ -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";
}
@@ -1,13 +1,14 @@
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 { Dialog, DialogContent, DialogTrigger } from "~/components/primitives/Dialog";
import { Header1, Header2 } from "../primitives/Headers";
import { Header1 } from "../primitives/Headers";
import { Input } from "../primitives/Input";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import { Spinner } from "../primitives/Spinner";
import { useState } from "react";
import { useFetcher } from "@remix-run/react";
import { generateTwoRandomWords } from "~/utils/randomWords";
type ModalProps = {
id: string;
@@ -25,11 +26,16 @@ export function RegenerateApiKeyModal({ id, title }: ModalProps) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="tertiary/small" leadingIconClassName="text-dimmed" LeadingIcon="key">
<Button
variant="tertiary/small"
leadingIconClassName="text-dimmed"
LeadingIcon={ArrowPathIcon}
>
Regenerate
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>{`Regenerate ${title} Environment Key`}</DialogHeader>
<RegenerateApiKeyModalContent
id={id}
title={title}
@@ -41,7 +47,7 @@ export function RegenerateApiKeyModal({ id, title }: ModalProps) {
);
}
const RegenerateApiKeyModalContent = ({ id, title, randomWord, closeModal }: ModalContentProps) => {
const RegenerateApiKeyModalContent = ({ id, randomWord, title, closeModal }: ModalContentProps) => {
const [confirmationText, setConfirmationText] = useState("");
const fetcher = useFetcher();
const isSubmitting = fetcher.state === "submitting";
@@ -52,55 +58,47 @@ const RegenerateApiKeyModalContent = ({ id, title, randomWord, closeModal }: Mod
}
return (
<div className="flex w-full flex-col items-center gap-y-6">
<div className="flex justify-center">
<Header1>{title} Environment</Header1>
<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>
<Header2 className="rounded border border-rose-500 bg-rose-500/10 px-3.5 py-2 text-center text-bright">
Are you sure you want to regenerate the keys <br />
for this environment?
</Header2>
<Paragraph variant="small" className="px-6 text-center">
<>
This will temporarily break any live jobs in the
<span className="strong text-bright"> {title} Environment</span> until the new API keys
are set in the relevant environment variables.
</>
</Paragraph>
<fetcher.Form
method="post"
action={`/resources/environments/${id}/regenerate-api-key`}
className="mt-2 flex w-full flex-col items-center gap-2"
>
<Paragraph variant="small" className="font-medium">
To confirm, please enter the text:{" "}
<span className="font-bold text-bright">{randomWord}</span>
</Paragraph>
<Input
type="text"
placeholder="Confirmation Text"
fullWidth={false}
value={confirmationText}
onChange={(e) => setConfirmationText(e.target.value)}
/>
<Button
variant="primary/large"
fullWidth
className="mt-4"
disabled={confirmationText !== randomWord}
>
{isSubmitting ? (
<Spinner color="white" />
) : (
<>
<NamedIcon
name="key"
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
/>
Regenerate API Keys
</>
)}
</Button>
<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)} supported>
<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} />;
}
}
@@ -127,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}
@@ -135,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>
@@ -117,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>
);
}
@@ -2,6 +2,7 @@ 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: UIMatch, allMatches: UIMatch[]) => ReactNode;
@@ -9,19 +10,26 @@ export function Breadcrumb() {
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, UIMatch } 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: UIMatch[] }) {
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,47 +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 />
</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>
);
}
@@ -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 { UIMatch } 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: UIMatch[] }) {
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,456 @@
import {
AcademicCapIcon,
ArrowRightIcon,
ArrowRightOnRectangleIcon,
ChartBarIcon,
CursorArrowRaysIcon,
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 { useV3Enabled } from "~/root";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { cn } from "~/utils/cn";
import {
accountPath,
inviteTeamMemberPath,
logoutPath,
newOrganizationPath,
newProjectPath,
organizationBillingPath,
organizationIntegrationsPath,
organizationPath,
organizationSettingsPath,
organizationTeamPath,
personalAccessTokensPath,
projectEnvironmentsPath,
projectEventsPath,
projectHttpEndpointsPath,
projectPath,
projectRunsPath,
projectSettingsPath,
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 { SideMenuHeader } from "./SideMenuHeader";
import { MenuCount, SideMenuItem } from "./SideMenuItem";
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 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"}>
<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"
/>
<SideMenuItem
name="Project settings"
icon="settings"
iconColor="text-teal-500"
to={projectSettingsPath(organization, project)}
data-action="project-settings"
/>
</div>
<div className="mb-1 flex flex-col gap-1 px-1">
<SideMenuHeader title={"Organization"}>
<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"
/>
<SideMenuItem
name="Organization settings"
icon="settings"
iconColor="text-teal-500"
to={organizationSettingsPath(organization)}
data-action="organization-settings"
/>
</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,
organizations,
}: {
project: SideMenuProject;
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">{project.name ?? "Select a project"}</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.length > 0 ? (
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"
/>
);
})
) : (
<PopoverMenuItem
to={newProjectPath(organization)}
title="New project"
icon="plus"
/>
)}
</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,6 +7,7 @@ import {
import {
ArrowTopRightOnSquareIcon,
BookOpenIcon,
ChartBarIcon,
CheckCircleIcon,
ChevronRightIcon,
} from "@heroicons/react/24/solid";
@@ -58,6 +59,12 @@ export const variantClasses = {
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;
@@ -66,12 +73,14 @@ export function Callout({
children,
className,
icon,
cta,
variant,
to,
}: {
children?: React.ReactNode;
className?: string;
icon?: React.ReactNode;
cta?: React.ReactNode;
variant: CalloutVariant;
to?: string;
}) {
@@ -84,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" ? (
@@ -135,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";
@@ -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,16 +59,18 @@ 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 { tablerIcons } from "~/utils/tablerIcons";
import tablerSpritePath from "./tabler-sprite.svg";
const icons = {
@@ -127,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) => (
@@ -183,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;
@@ -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>
);
}
@@ -56,7 +56,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-max overflow-hidden rounded-md bg-popover text-bright shadow-md animate-in fade-in-40",
"relative z-50 min-w-max overflow-hidden rounded-md border border-slate-700 bg-popover text-bright shadow-md animate-in fade-in-40",
position === "popper" && "translate-y-1",
className
)}
@@ -65,7 +65,7 @@ const SelectContent = React.forwardRef<
>
<SelectPrimitive.Viewport
className={cn(
"border border-slate-800 px-1 py-0",
"px-1 py-0",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
@@ -51,7 +51,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-uiBorder",
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-ui-border",
{
variants: {
position: {
@@ -154,7 +154,7 @@ const SheetContent = React.forwardRef<
{...props}
>
<div className="grid max-h-full grid-rows-[2.75rem_1fr] overflow-hidden">
<div className="flex items-center gap-2 border-b border-uiBorder p-2">
<div className="flex items-center gap-2 border-b border-ui-border p-2">
<SheetPrimitive.Close className="rounded-sm p-1 transition hover:bg-slate-800 disabled:pointer-events-none">
<NamedIcon name="close" className="h-4 w-4" />
<span className="sr-only">Close</span>
@@ -181,7 +181,7 @@ export const SheetBody = ({ className, ...props }: React.HTMLAttributes<HTMLDivE
export const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"mx-4 flex shrink-0 items-center gap-4 border-b border-uiBorder py-3.5",
"mx-4 flex shrink-0 items-center gap-4 border-b border-ui-border py-3.5",
className
)}
{...props}
@@ -194,7 +194,7 @@ export const SheetFooter = ({
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("shrink-0", className)} {...props}>
<div className="mx-4 border-t border-uiBorder py-3">{children}</div>
<div className="mx-4 border-t border-ui-border py-3">{children}</div>
</div>
);
@@ -5,7 +5,7 @@ export function Spinner({
color = "blue",
}: {
className?: string;
color?: "blue" | "white";
color?: "blue" | "white" | "muted";
}) {
const colors = {
blue: {
@@ -16,6 +16,10 @@ export function Spinner({
light: "rgba(255, 255, 255, 0.4)",
dark: "rgba(255, 255, 255)",
},
muted: {
light: "#1C2433",
dark: "#3C4B62",
},
};
const currentColor = colors[color];
+26 -10
View File
@@ -16,7 +16,7 @@ export const Table = forwardRef<HTMLTableElement, TableProps>(
return (
<div
className={cn(
"overflow-x-auto whitespace-nowrap rounded-md border border-uiBorder scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
"overflow-x-auto whitespace-nowrap rounded-md border border-ui-border scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
containerClassName,
fullWidth && "w-full"
)}
@@ -39,7 +39,7 @@ export const TableHeader = forwardRef<HTMLTableSectionElement, TableHeaderProps>
return (
<thead
ref={ref}
className={cn("rounded-t-md", "relative divide-y divide-uiBorder bg-slate-850", className)}
className={cn("rounded-t-md", "relative divide-y divide-ui-border bg-slate-850", className)}
>
{children}
</thead>
@@ -55,7 +55,7 @@ type TableBodyProps = {
export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
({ className, children }, ref) => {
return (
<tbody ref={ref} className={cn("relative divide-y divide-uiBorder", className)}>
<tbody ref={ref} className={cn("relative divide-y divide-ui-border", className)}>
{children}
</tbody>
);
@@ -106,7 +106,7 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
ref={ref}
scope="col"
className={cn(
"px-4 py-3 align-middle text-xs font-normal uppercase tracking-wider text-dimmed",
"px-4 py-2 align-middle text-xxs font-normal uppercase tracking-wider text-dimmed",
alignmentClassName,
className
)}
@@ -122,10 +122,26 @@ type TableCellProps = TableCellBasicProps & {
to?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
hasAction?: boolean;
isSticky?: boolean;
};
const stickyStyles =
"sticky right-0 z-10 w-[2.8rem] min-w-[2.8rem] bg-background before:absolute before:pointer-events-none before:-left-8 before:top-0 before:h-full before:min-w-[2rem] before:bg-gradient-to-r before:from-transparent before:to-background before:content-[''] group-hover:before:to-slate-900";
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
({ className, alignment = "left", children, colSpan, to, onClick, hasAction = false }, ref) => {
(
{
className,
alignment = "left",
children,
colSpan,
to,
onClick,
hasAction = false,
isSticky = false,
},
ref
) => {
let alignmentClassName = "text-left";
switch (alignment) {
case "center":
@@ -154,6 +170,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
? "cursor-pointer group-hover:bg-slate-900"
: "px-4 py-3 align-middle",
!to && !onClick && alignmentClassName,
isSticky && stickyStyles,
className
)}
colSpan={colSpan}
@@ -174,9 +191,6 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
}
);
const stickyStyles =
"sticky right-0 z-10 w-[2.8rem] min-w-[2.8rem] bg-background before:absolute before:pointer-events-none before:-left-8 before:top-0 before:h-full before:min-w-[2rem] before:bg-gradient-to-r before:from-transparent before:to-background before:content-[''] group-hover:before:to-slate-900";
export const TableCellChevron = forwardRef<
HTMLTableCellElement,
{
@@ -189,7 +203,8 @@ export const TableCellChevron = forwardRef<
>(({ className, to, children, isSticky, onClick }, ref) => {
return (
<TableCell
className={cn(isSticky && stickyStyles, className)}
className={className}
isSticky={isSticky}
to={to}
onClick={onClick}
ref={ref}
@@ -213,7 +228,8 @@ export const TableCellMenu = forwardRef<
const [isOpen, setIsOpen] = useState(false);
return (
<TableCell
className={cn(isSticky && stickyStyles, className)}
className={className}
isSticky={isSticky}
onClick={onClick}
ref={ref}
alignment="right"
@@ -8,11 +8,12 @@ export type TabsProps = {
to: string;
}[];
className?: string;
layoutId: string
};
export function Tabs({ tabs, className }: TabsProps) {
export function Tabs({ tabs, className, layoutId }: TabsProps) {
return (
<div className={cn(`flex flex-row gap-x-6 border-b border-uiBorder`, className)}>
<div className={cn(`flex flex-row gap-x-6 border-b border-ui-border`, className)}>
{tabs.map((tab, index) => (
<NavLink key={index} to={tab.to} className="group flex flex-col items-center pt-1" end>
{({ isActive, isPending }) => (
@@ -26,7 +27,7 @@ export function Tabs({ tabs, className }: TabsProps) {
{tab.label}
</span>
{isActive || isPending ? (
<motion.div layoutId="underline" className="mt-1 h-0.5 w-full bg-indigo-500" />
<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" />
)}
+11 -13
View File
@@ -5,6 +5,7 @@ import { Toaster, toast } from "sonner";
import { useTypedLoaderData } from "remix-typedjson";
import { loader } from "~/root";
import { useEffect } from "react";
import { Paragraph } from "./Paragraph";
const defaultToastDuration = 5000;
const permanentToastDuration = 60 * 60 * 24 * 1000;
@@ -38,26 +39,23 @@ export function ToastUI({
}) {
return (
<div
className={`self-end rounded-lg border border-slate-750 bg-midnight-900 shadow-md`}
className={`self-end rounded-md border border-border bg-background`}
style={{
width: toastWidth,
}}
>
<div
className="flex w-full gap-2 rounded-lg bg-no-repeat p-4 text-bright"
style={{
background:
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
}}
>
<div className="flex w-full items-start gap-2 rounded-lg p-3">
{variant === "success" ? (
<CheckCircleIcon className="h-6 w-6 text-green-600" />
<CheckCircleIcon className="mt-1 h-6 min-h-[1.5rem] w-6 min-w-[1.5rem] text-green-600" />
) : (
<ExclamationCircleIcon className="h-6 w-6 text-rose-600" />
<ExclamationCircleIcon className="mt-1 h-6 w-6 min-w-[1.5rem] text-rose-600" />
)}
{message}
<button className="ms-auto p-1" onClick={() => toast.dismiss(t)}>
<XMarkIcon className="h-4 w-4 text-bright" />
<Paragraph className="py-1 text-dimmed">{message}</Paragraph>
<button
className="ms-auto rounded p-2 text-dimmed transition hover:bg-midnight-800 hover:text-bright"
onClick={() => toast.dismiss(t)}
>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
</div>
@@ -1,9 +1,15 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "~/utils/cn";
const variantClasses = {
basic:
"bg-popover border border-slate-800 rounded-md px-3 py-1.5 text-sm text-bright shadow-md fade-in-50",
dark: "bg-background border border-border rounded px-3 py-2 text-sm text-bright shadow-md fade-in-50",
};
type Variant = keyof typeof variantClasses;
const TooltipProvider = TooltipPrimitive.Provider;
const TooltipArrow = React.forwardRef<
@@ -22,15 +28,20 @@ Tooltip.displayName = TooltipPrimitive.Root.displayName;
const TooltipTrigger = TooltipPrimitive.Trigger;
type TooltipContentProps = {
variant?: Variant;
} & React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
TooltipContentProps
>(({ className, sideOffset = 4, variant = "basic", ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border border-slate-800 bg-popover px-3 py-1.5 text-sm text-bright shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
"z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
variantClasses[variant],
className
)}
{...props}
@@ -43,17 +54,56 @@ function SimpleTooltip({
content,
side,
hidden,
variant,
className,
}: {
button: React.ReactNode;
content: React.ReactNode;
side?: React.ComponentProps<typeof TooltipContent>["side"];
hidden?: boolean;
variant?: Variant;
className?: string;
}) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{button}</TooltipTrigger>
<TooltipContent side={side} hidden={hidden} className="text-xs">
<TooltipTrigger className="h-fit">{button}</TooltipTrigger>
<TooltipContent
side={side}
hidden={hidden}
className={cn("text-xs", className)}
variant={variant}
>
{content}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
export function LoginTooltip({
children,
side,
content,
className,
}: {
children: React.ReactNode;
side: "top" | "bottom" | "left" | "right";
content: React.ReactNode | string;
className?: string;
}) {
return (
<TooltipProvider delayDuration={2500} disableHoverableContent>
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent
className={cn(
"max-w-xs border-slate-800 bg-slate-900 px-5 py-4 backdrop-blur-md",
className
)}
side={side}
sideOffset={14}
>
{content}
</TooltipContent>
</Tooltip>
+78 -65
View File
@@ -1,6 +1,7 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { BoltIcon, ForwardIcon } from "@heroicons/react/24/solid";
import { PlayIcon } from "@heroicons/react/20/solid";
import { BoltIcon } from "@heroicons/react/24/solid";
import {
Form,
Outlet,
@@ -9,13 +10,14 @@ import {
useNavigate,
useNavigation,
} from "@remix-run/react";
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
import { RuntimeEnvironmentType, User } from "@trigger.dev/database";
import { useMemo } from "react";
import { usePathName } from "~/hooks/usePathName";
import type { RunBasicStatus } from "~/models/jobRun.server";
import { ViewRun } from "~/presenters/RunPresenter.server";
import { cancelSchema } from "~/routes/resources.runs.$runId.cancel";
import { schema } from "~/routes/resources.runs.$runId.rerun";
import { formatDuration } from "~/utils";
import { formatDuration, formatDurationMilliseconds } from "~/utils";
import { cn } from "~/utils/cn";
import { runCompletedPath, runTaskPath, runTriggerPath } from "~/utils/pathBuilder";
import { CodeBlock } from "../code/CodeBlock";
@@ -25,6 +27,7 @@ import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { DateTime } from "../primitives/DateTime";
import { Header2 } from "../primitives/Headers";
import { Icon } from "../primitives/Icon";
import { NamedIcon } from "../primitives/NamedIcon";
import {
PageButtons,
@@ -37,14 +40,7 @@ import {
} from "../primitives/PageHeader";
import { Paragraph } from "../primitives/Paragraph";
import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
import {
RunBasicStatus,
RunStatusIcon,
RunStatusLabel,
hasFinished,
runBasicStatus,
runStatusTitle,
} from "../runs/RunStatuses";
import { RunStatusIcon, RunStatusLabel, runStatusTitle } from "../runs/RunStatuses";
import {
RunPanel,
RunPanelBody,
@@ -70,11 +66,12 @@ type RunOverviewProps = {
run: string;
runsPath: string;
};
currentUser: User;
};
const taskPattern = /\/tasks\/(.*)/;
export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps) {
export function RunOverview({ run, trigger, showRerun, paths, currentUser }: RunOverviewProps) {
const navigate = useNavigate();
const pathName = usePathName();
@@ -94,7 +91,8 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
}
}, [pathName]);
const basicStatus = runBasicStatus(run.status);
const usernameForEnv =
currentUser.id !== run.environment.userId ? run.environment.userName : undefined;
return (
<PageContainer>
@@ -105,7 +103,9 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
to: paths.back,
text: "Runs",
}}
title={`Run #${run.number}`}
title={
typeof run.number === "number" ? `Run #${run.number}` : `Run ${run.id.slice(0, 8)}`
}
/>
<PageButtons>
{run.isTest && (
@@ -114,15 +114,16 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
Test run
</span>
)}
{showRerun && hasFinished(run.status) && (
{showRerun && run.isFinished && (
<RerunPopover
runId={run.id}
runPath={paths.run}
runsPath={paths.runsPath}
environmentType={run.environment.type}
status={basicStatus}
status={run.basicStatus}
/>
)}
{!hasFinished(run.status) && <CancelRun runId={run.id} />}
{!run.isFinished && <CancelRun runId={run.id} />}
</PageButtons>
</PageTitleRow>
<PageInfoRow>
@@ -140,16 +141,26 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
<PageInfoProperty icon={"property"} label={"Version"} value={`v${run.version}`} />
<PageInfoProperty
label={"Env"}
value={<EnvironmentLabel environment={run.environment} />}
value={<EnvironmentLabel environment={run.environment} userName={usernameForEnv} />}
/>
<PageInfoProperty
icon={"clock"}
label={"Duration"}
value={formatDuration(run.startedAt, run.completedAt)}
value={formatDuration(run.startedAt, run.completedAt, { style: "short" })}
/>
<PageInfoProperty
icon={<Icon icon="alarm-filled" className="h-4 w-4 text-blue-500" />}
label={"Execution Time"}
value={formatDurationMilliseconds(run.executionDuration, { style: "short" })}
/>
<PageInfoProperty
icon={<Icon icon="list-numbers" className="h-4 w-4 text-yellow-500" />}
label={"Execution Count"}
value={<>{run.executionCount}</>}
/>
</PageInfoGroup>
<PageInfoGroup alignment="right">
<Paragraph variant="extra-small" className="text-slate-600">
<Paragraph variant="extra-small" className="whitespace-nowrap text-slate-600">
RUN ID: {run.id}
</Paragraph>
</PageInfoGroup>
@@ -159,16 +170,17 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
<div className="grid h-full grid-cols-2 gap-2">
<div className="flex flex-col gap-6 overflow-y-auto py-4 pl-4 pr-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<div>
{(run.tasks.length === 0 || run.tasks.every((t) => t.noop)) && (
<Callout
variant={"warning"}
to="https://trigger.dev/docs/documentation/concepts/tasks"
className="mb-4"
>
This Run completed but it did not use any Tasks this can cause unpredictable
results. Read the docs to view the solution.
</Callout>
)}
{run.status === "SUCCESS" &&
(run.tasks.length === 0 || run.tasks.every((t) => t.noop)) && (
<Callout
variant={"warning"}
to="https://trigger.dev/docs/documentation/concepts/tasks"
className="mb-4"
>
This Run completed but it did not use any Tasks this can cause unpredictable
results. Read the docs to view the solution.
</Callout>
)}
<Header2 className="mb-2">Trigger</Header2>
<RunPanel
selected={selectedId === "trigger"}
@@ -209,10 +221,10 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
);
})
) : (
<BlankTasks status={run.status} basicStatus={basicStatus} />
<BlankTasks status={run.basicStatus} />
)}
</div>
{(basicStatus === "COMPLETED" || basicStatus === "FAILED") && (
{(run.basicStatus === "COMPLETED" || run.basicStatus === "FAILED") && (
<div>
<Header2 className={cn("mb-2")}>Run Summary</Header2>
<RunPanel
@@ -261,7 +273,9 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
<CodeBlock language="json" code={run.output} maxLines={10} />
) : (
run.output === null && (
<Paragraph variant="small">This Run returned nothing.</Paragraph>
<Paragraph variant="small" className="mt-4">
This Run returned nothing.
</Paragraph>
)
)}
</RunPanelBody>
@@ -272,7 +286,7 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
{/* Detail view */}
<div className="overflow-y-auto py-4 pr-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<Header2 className="mb-2">Detail</Header2>
<Header2 className="mb-2">Details</Header2>
{selectedId ? <Outlet /> : <Callout variant="info">Select a task or trigger</Callout>}
</div>
</div>
@@ -281,14 +295,8 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
);
}
function BlankTasks({
status,
basicStatus,
}: {
status: JobRunStatus;
basicStatus: RunBasicStatus;
}) {
switch (basicStatus) {
function BlankTasks({ status }: { status: RunBasicStatus }) {
switch (status) {
default:
case "COMPLETED":
return <Paragraph variant="small">There were no tasks for this run.</Paragraph>;
@@ -310,18 +318,20 @@ function BlankTasks({
function RerunPopover({
runId,
runPath,
runsPath,
environmentType,
status,
}: {
runId: string;
runPath: string;
runsPath: string;
environmentType: RuntimeEnvironmentType;
status: RunBasicStatus;
}) {
const lastSubmission = useActionData();
const [form, { successRedirect }] = useForm({
const [form, { successRedirect, failureRedirect }] = useForm({
id: "rerun",
// TODO: type this
lastSubmission: lastSubmission as any,
@@ -337,50 +347,53 @@ function RerunPopover({
Rerun Job
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-80 flex-col gap-2 p-4" align="end">
<PopoverContent className="flex min-w-[20rem] max-w-[20rem] flex-col gap-2 p-0" align="end">
<Form method="post" action={`/resources/runs/${runId}/rerun`} {...form.props}>
<input {...conform.input(successRedirect, { type: "hidden" })} defaultValue={runsPath} />
<input {...conform.input(failureRedirect, { type: "hidden" })} defaultValue={runPath} />
{environmentType === "PRODUCTION" && (
<Callout variant="warning" className="mb-2">
This will rerun this Job in your Production environment.
</Callout>
<div className="px-4 pt-4">
<Callout variant="warning">
This will rerun this Job in your Production environment.
</Callout>
</div>
)}
<div className="flex flex-col items-start gap-4 divide-y divide-slate-600">
<div>
<div className="flex flex-col items-start divide-y divide-slate-800">
<div className="p-4">
<Paragraph variant="small" className="mb-3">
Start a brand new Job run with the same Trigger data as this one. This will re-do
every Task.
</Paragraph>
<Button
variant="primary/small"
variant="secondary/medium"
type="submit"
name={conform.INTENT}
value="start"
fullWidth
LeadingIcon={BoltIcon}
className="text-bright"
>
<BoltIcon className="mr-1 h-3.5 w-3.5 text-bright" />
Run again
</Button>
<Paragraph variant="extra-small" className="mt-2">
Start a brand new job run with the same Trigger data as this one. This will re-do
every task.
</Paragraph>
</div>
{status === "FAILED" && (
<div className="pt-4">
<div className="p-4">
<Paragraph variant="small" className="mb-3">
Continue running this Job run from where it left off. This will skip any Task that
has already been completed.
</Paragraph>
<Button
variant="primary/small"
variant="secondary/medium"
type="submit"
name={conform.INTENT}
value="continue"
fullWidth
LeadingIcon={ForwardIcon}
className="text-bright"
>
Retry job run
<PlayIcon className="mr-1 h-3.5 w-3.5 text-bright" />
Retry Job run
</Button>
<Paragraph variant="extra-small" className="mt-2">
Continue running this job run from where it left off. This will skip any task that
has already been completed.
</Paragraph>
</div>
)}
</div>
+25 -11
View File
@@ -33,7 +33,19 @@ import { Spinner } from "../primitives/Spinner";
import type { DetailedTask } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route";
export function TaskDetail({ task }: { task: DetailedTask }) {
const { name, description, icon, status, params, properties, output, style, attempts } = task;
const {
name,
description,
icon,
status,
params,
properties,
output,
outputIsUndefined,
style,
attempts,
noop,
} = task;
const startedAt = task.startedAt ? new Date(task.startedAt) : undefined;
const completedAt = task.completedAt ? new Date(task.completedAt) : undefined;
@@ -140,16 +152,18 @@ export function TaskDetail({ task }: { task: DetailedTask }) {
<Paragraph variant="small">No input</Paragraph>
)}
</div>
<div className="mt-4 flex flex-col gap-2">
<Header3>Output</Header3>
{output ? (
<ClientOnly fallback={<Spinner />}>
{() => <CodeBlock code={output} maxLines={35} />}
</ClientOnly>
) : (
<Paragraph variant="small">No output</Paragraph>
)}
</div>
{!noop && (
<div className="mt-4 flex flex-col gap-2">
<Header3>Output</Header3>
{output && !outputIsUndefined ? (
<ClientOnly fallback={<Spinner />}>
{() => <CodeBlock code={output} maxLines={35} />}
</ClientOnly>
) : (
<Paragraph variant="small">No output</Paragraph>
)}
</div>
)}
</RunPanelBody>
</RunPanel>
);
@@ -0,0 +1,230 @@
import {
CheckCircleIcon,
ClockIcon,
ExclamationTriangleIcon,
NoSymbolIcon,
PauseCircleIcon,
XCircleIcon,
XMarkIcon,
} from "@heroicons/react/20/solid";
import { useNavigate } from "@remix-run/react";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { cn } from "~/utils/cn";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
import { Paragraph } from "../primitives/Paragraph";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../primitives/Select";
import { Spinner } from "../primitives/Spinner";
import {
FilterableEnvironment,
FilterableStatus,
RunListSearchSchema,
environmentKeys,
statusKeys,
} from "./RunStatuses";
import { TimeFrameFilter } from "./TimeFrameFilter";
import { Button } from "../primitives/Buttons";
import { useCallback } from "react";
export function RunsFilters() {
const navigate = useNavigate();
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const { environment, status, from, to } = RunListSearchSchema.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 handleStatusChange = useCallback((value: FilterableStatus | "ALL") => {
handleFilterChange("status", value === "ALL" ? undefined : value);
}, []);
const handleEnvironmentChange = useCallback((value: FilterableEnvironment | "ALL") => {
handleFilterChange("environment", value === "ALL" ? undefined : value);
}, []);
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 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>
<SelectGroup>
<Select name="status" value={status ?? "ALL"} onValueChange={handleStatusChange}>
<SelectTrigger size="secondary/small" width="full">
<SelectValue placeholder="Select status" className="ml-2 p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph variant="extra-small" className="pl-0.5">
All statuses
</Paragraph>
</SelectItem>
{statusKeys.map((status) => (
<SelectItem key={status} value={status}>
{
<span className="flex items-center gap-1 text-xs">
<FilterStatusIcon status={status} className="h-4 w-4" />
<FilterStatusLabel status={status} />
</span>
}
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<TimeFrameFilter from={from} to={to} onRangeChanged={handleTimeFrameChange} />
<Button variant="tertiary/small" onClick={() => clearFilters()} LeadingIcon={"close"}>
Clear
</Button>
</div>
);
}
export function FilterStatusLabel({ status }: { status: FilterableStatus }) {
return <span className={filterStatusClassNameColor(status)}>{filterStatusTitle(status)}</span>;
}
export function FilterStatusIcon({
status,
className,
}: {
status: FilterableStatus;
className: string;
}) {
switch (status) {
case "COMPLETED":
return <CheckCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
case "WAITING":
return <ClockIcon className={cn(filterStatusClassNameColor(status), className)} />;
case "QUEUED":
return <PauseCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
case "IN_PROGRESS":
return <Spinner className={cn(filterStatusClassNameColor(status), className)} />;
case "TIMEDOUT":
return (
<ExclamationTriangleIcon className={cn(filterStatusClassNameColor(status), className)} />
);
case "CANCELED":
return <NoSymbolIcon className={cn(filterStatusClassNameColor(status), className)} />;
case "FAILED":
return <XCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
}
}
}
export function filterStatusTitle(status: FilterableStatus): string {
switch (status) {
case "QUEUED":
return "Queued";
case "IN_PROGRESS":
return "In progress";
case "WAITING":
return "Waiting";
case "COMPLETED":
return "Completed";
case "FAILED":
return "Failed";
case "CANCELED":
return "Canceled";
case "TIMEDOUT":
return "Timed out";
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
}
}
}
export function filterStatusClassNameColor(status: FilterableStatus): string {
switch (status) {
case "QUEUED":
return "text-slate-500";
case "IN_PROGRESS":
return "text-blue-500";
case "WAITING":
return "text-blue-500";
case "COMPLETED":
return "text-green-500";
case "FAILED":
return "text-rose-500";
case "CANCELED":
return "text-slate-500";
case "TIMEDOUT":
return "text-amber-300";
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
}
}
}

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