Compare commits

...

139 Commits

Author SHA1 Message Date
Matt Aitken 78dac99d72 Release 3.0.0-beta.55
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 4s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 2s
🚀 Publish Trigger.dev Docker / units (push) Failing after 19s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-infra (push) Has been skipped
2024-08-20 15:50:14 +01:00
James Ritchie d6786002b2 Full Next.js guide (#1259)
* tasks no longer inside a group in the side menu (and added “cron”)

* Delay using a timezone

* Added React Not Defined error to the troubleshooting page

* Improved the React common problem

* Link to v2 docs

* New Development section and entry in Common Problems

* Concurrently running the terminal

* Fixed the .env weirdness

* Added section on creating PATs for Github actions

* Improved the Machine spec and limits page

* Quick start steps now have nice images

* Added a diagram for the lifecycle functions

* Added note about onFailure

* CRON -> cron/Cron

* WIP adding more steps to the next.js guide

* WIP next.js

* WIP adding tabbed steps for pages/app router

* References to Infisical links to their homepage so it’s clearer

* WIP updating the nextjs guide

* WIP nextjs guide

* More nextjs guide steps

* More copy

* Added rate limit trouble shooting

* Removed old prisma error title

* Added secret key step

* Added a note for logging in using a specified domain if self hosting

* typo

* App router docs copy

* Deploy copy update

* Added a favicon.png to fix a docs build error

* Removed unused snippet

* Server actions now inside a tab

* Server actions + restructured the triggering section

* Added troubleshooting snippet for react event handlers

* Added a new troubleshooting snippet for ESM

* Updated old replaying image to reflect the new UI

* Removed references to reattempting

* Updated replaying from the run page

* Added a bulk replay section

* Updated the writing tasks intro page

* Removed edge runtime code for now

* Added edge runtime – it seems to just work!

* import type
2024-08-20 14:50:39 +01:00
github-actions[bot] 9d529e9f17 chore: Update version for release (beta) (#1264)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-20 14:46:19 +01:00
Matt Aitken d48eb9bac8 Fix: was showing deleted team member dev environments (#1261)
* Env vars: don’t show deleted dev environments

* Concurrency limits: don’t show deleted dev environments

* Create/edit schedule: don’t show deleted dev environments
2024-08-20 14:03:24 +01:00
Matt Aitken 0591db5f2b Fix for short runs inside (batch)triggerAndWait (#1263)
* Test for checkpoints

* Make sourceTaskAttemptId optional on resumeBatchRun

* Removed all completions/executions logic from the shared queue consumer

* Removed the sourceTaskAttemptId from ResumeBatchRunService

* Revert "Removed all completions/executions logic from the shared queue consumer"

This reverts commit d35398d50463c81a1975bb5d5bcfca66a24b8ede.

* WIP on triggerAndWait…

* Fixed triggerAndWait continuing when a checkpoint completes

* Remove the ResumeAttempt code that fails attempts (was protecting against infinite restores)

* Removed messageBody.data.completedAttemptIds.length === 0 commented out code

* Don’t ack if there’s no batchRun

* Added the marqs?.replaceMessage back in but NOT when there’s no checkpoint. More logging

This is a fix for when some attempts fail

* Improvement to the test task that now randomly fails attempts

* When a checkpoint happens, only continue the attempt if it’s in the correct state

* Changeset for rollback in branch

* Set keepRunAlive to false when the dependent task isn’t finished

* Changeset manual version (to get inline with the hotfix branch)

* Changeset: Fixes for continuing after waits

* Latest lockfile (after manual changeset version)
2024-08-20 13:57:08 +01:00
nicktrn a64064965d Prevent runs that fail fast from never sending a heartbeat 2024-08-19 13:17:33 +01:00
Matt Aitken 41ec450bee Revert "Remove the RequeueTaskRunService"
This reverts commit 3421f5ec0b.
2024-08-15 20:43:47 +01:00
Matt Aitken 9a9bdc20c9 Revert "Disable upserting task queues when triggering a task"
This reverts commit 6652edf7a7.
2024-08-15 20:42:50 +01:00
Matt Aitken 6652edf7a7 Disable upserting task queues when triggering a task 2024-08-15 19:34:22 +01:00
Matt Aitken 369bc38560 Instead of upserting taskQueues, do a findFirst then create/update 2024-08-15 18:48:50 +01:00
Matt Aitken 27f7132509 Don’t do the auto-increment inside a transaction, this means triggerTask isn’t inside one 2024-08-15 17:44:10 +01:00
Matt Aitken b6fc68aaae Don’t use Graphile for invocation usage reporting 2024-08-15 17:22:52 +01:00
Matt Aitken 3421f5ec0b Remove the RequeueTaskRunService 2024-08-15 17:20:40 +01:00
Matt Aitken 15097a3a67 Use the replica for the v2 run and task presenters 2024-08-15 15:34:57 +01:00
Matt Aitken 1b7bb90852 Only show the first 1000 task on the v2 run page 2024-08-15 15:26:12 +01:00
Matt Aitken 7e68c02636 Don’t reload v2 run page if more than 100 tasks 2024-08-15 15:25:59 +01:00
nicktrn 01e01376f8 Fix attempt retry mechanics for new workers 2024-08-15 12:13:13 +01:00
James Ritchie db9163e775 Added a note to use batchTrigger() instead of trigger() 2024-08-14 13:25:08 +01:00
nicktrn e9bdd0c7e5 Release 3.0.0-beta.53 2024-08-14 11:40:38 +01:00
github-actions[bot] 7cb977ff2c chore: Update version for release (beta) (#1256)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-14 11:39:44 +01:00
James Ritchie 2940b07430 Added a description panel to the Concurrency admin page 2024-08-14 11:33:15 +01:00
James Ritchie c46e3c97b5 Fix for schedule page not scrolling 2024-08-14 11:23:16 +01:00
nicktrn 16cb1c46ac Fix restored container names 2024-08-13 18:10:09 +01:00
Matt Aitken 2b0d68a0b6 Include the old message data when replacing, so we get the projectId etc. 2024-08-13 17:09:29 +01:00
James Ritchie 2088fa392d Added depenenciesToBundle guide to bundle all packages 2024-08-13 17:06:42 +01:00
Matt Aitken f1ffc2e6c8 Added messageReplaced to concurrency tracking (when freezing) 2024-08-13 16:36:39 +01:00
Matt Aitken c0b9394099 Call subscriber.messageDequeued in dequeueMessageInSharedQueue 2024-08-13 15:31:25 +01:00
Matt Aitken 937b61913d Added more logging to TaskRunConcurrencyTracker and some more try/catches 2024-08-13 14:54:50 +01:00
nicktrn 5cf90da722 v3: fix unfreezable state crashes for runs with multiple waits (#1253)
* support named capture groups

* write crash errors to attempt.error

* make restored pod names unique per checkpoint

* use last eight characters of checkpoint id instead

* add more chaos monkey env vars

* Ignore unfreezable states

* prevent excessive queue config parsing errors

* handle dependency resume edge case

* better entry point logging

* ignore checkpoint cancellation timeouts

* add missing idempotency keys to wait for dep replays

* remove checkpoints between attempts

* fix retry container names on kubernetes

* add changeset

* fix types

* bring back internal duration timers
2024-08-13 11:44:40 +01:00
Matt Aitken da6ce3c8d5 Concurrency page and more accurate tracking (#1252)
* Initial TaskRunConcurrencyTracker implementation

* MARQS calls a subscriber to events

* When enqueuing add the extra required metadata

* Track concurrency per environment for tasks too

* Admin page for global concurrency

* Use the new concurrency tracker on the tasks page

* Useful performance test task

* getAllTaskIdentifiers()

* New page for concurrency

* BackgroundWorkerTask index for quick lookup of task identifiers

* Added a way to get concurrency for environments

* Added upgrade/request more concurrency button

* Queued task column working

* Use defer and suspense

* Added queue column to the concurrency environments table

* Some comments added for clarity

* Fixed bad log message

* Sidemenu: move lower and rename to “Concurrency limits”

* Only show the environments, not tasks. Renamed to “Concurrency limits”
2024-08-13 11:43:46 +01:00
James Ritchie 07054f13d1 Fixed v2 usage meter displaying when on paid plan (#1255)
* Fixed v2 usage meter displaying when on paid plan

* Show the free usage panel only for v3 projects
2024-08-13 11:14:28 +01:00
nicktrn c50a94b962 Increase indexing memory limit 2024-08-12 17:05:25 +01:00
Matt Aitken bb38261947 FinalizeRunService (#1250)
* WIP notes on each location where we’ll use finalize

* Initial FinalizeTaskRunService

* ExpireEnqueuedRunService uses FinalizeTaskRunService

* FailedTaskRunService uses FinalizeTaskRunService

* Allow passing in an include when finalizing the run

* CrashTaskRunService using FinalizeTaskRunService

* Remove comments

* Status is optional

* CancelAttemptService using FinalizeTaskRunService

* Import tidy

* CancelTaskRunService using FinalizeTaskRunService

* Import tidying

* CompleteAttemptService system failure switched to FinalizeTaskRunService

* Added more logging to Finalizing

* CompleteAttemptStatus COMPLETED_SUCCESSFULLY

* CompletedAttempt “SYSTEM_FAILURE”

* CompletedService final pair

* Use satisfies so we can derive types from the groups

* Only allow final states to be used with this service

* BaseService tx support, minor improvements
2024-08-08 15:32:06 +01:00
James Ritchie 0d4e3e70c1 Lots of small docs improvements (#1251)
* tasks no longer inside a group in the side menu (and added “cron”)

* Delay using a timezone

* Added React Not Defined error to the troubleshooting page

* Improved the React common problem

* Link to v2 docs

* New Development section and entry in Common Problems

* Concurrently running the terminal

* Fixed the .env weirdness

* Added section on creating PATs for Github actions

* Improved the Machine spec and limits page

* Quick start steps now have nice images

* Added a diagram for the lifecycle functions

* Added note about onFailure

* CRON -> cron/Cron

* Updated the cli-dev steps for the concurrently package

* Fixed capital letter

* Updated diagram text

* Removed dead page
2024-08-08 15:03:05 +01:00
nicktrn bf1482fdb9 Save crash errors on the attempt 2024-08-08 11:42:46 +01:00
Matt Aitken e2d3358271 Added error from the run attempt with nice display (#1249) 2024-08-07 15:50:45 +01:00
James Ritchie 6270abaab8 Remove v2 docs pages (#1246)
* Make the tooltip text color grey so it’s readable again the primary color

* Setup and styling for the Guides section

* Better Guides icon

* Improved the prerequisites

* WIP Nextjs guide and new folder structures

* Copy updates

* Useful next steps is now a snippet

* Better icons for the next steps section

* Renamed the “prerequisites” snippet

* Node.js guide

* Added a “Creating a project” guide

* New snippet for prerequisites

* New Remix guide

* Added Remix to the side bar

* Tweaked icons for creating a project page

* Added the hello world step to the onboarding steps

* Better “useful next steps” snippet card links

* Moved prerequisites

* Fixing links to images

* Moved v2 migration page to guides

* Removed dead link

* Attempt fix for redirect

* Fixed redirects

* Getting started section includes link to roadmap

* Removed icon from side menu

* Deleted all v2 pages (excluding updating mint.json)

* Removed v2 pages, redirects and versions from mint.json

* Deleted v2 snippets

* Deleted un-used pages

* Set of more useful coming soon snippets

* All snippets use the updated format

* updated folder “v3/“ with “/pages”

* Moved all main docs files to the route and updated the redirect

* Fixed URLs in the mdx pages to the new route path

* URL goes to the proper pricing page

* Better delayed runs image

* Attempt fix for self hosting page not redirecting
2024-08-07 11:13:45 +01:00
Matt Aitken 6c71ddbe67 TaskRun completedAt index 2024-08-06 17:31:12 +01:00
James Ritchie fafda21e3b V2 end of life messaging, paid plan badge, and project menu improvements (#1245)
* Updated the v2 project message to include end of life

* Removed the v3 badge. Showing a v2 badge instead

* Removed the v3 badge variant

* Show a badge in the side menu if you’re paying

* Popover menu section header and button state colours now reflects the design UI

* Added a link to the blog in the end-of-life message

* Removed missing variant from badge
2024-08-06 10:50:23 +01:00
nicktrn dc2d68107f Revert "Ignore unfreezable states"
This reverts commit 7d9cec8652.
2024-08-06 10:11:28 +01:00
Matt Aitken b65e7f7a7a Show a different message if your logs are deleted but you’re within your retention period.
This happens if you’ve upgraded but the logs were already deleted.
2024-08-06 10:10:40 +01:00
nicktrn 7d9cec8652 Ignore unfreezable states 2024-08-06 08:56:06 +01:00
Matt Aitken a591e1118f Improved the experience when you have no run logs (#1244)
* Remove the old log truncation

* Added TaskRUn logsDeletedAt column

* Accurate timestamps for the run inspector

* EnsureProperty type when you want to make a single property not nullable

* No logs and upgrade messages working

* Button can be autofocused

* Replay dialog code editor is autofocused

* Fix for wrapping of span duration
2024-08-05 18:06:23 +01:00
nicktrn 4d2ea86d6c Use accurate datetime for run timeline 2024-08-05 15:16:43 +01:00
Eric Allam 366ae5ca7d Create a shared queue span every 10 seconds 2024-08-02 17:31:52 +01:00
Eric Allam 157973a720 Force recording shared queue consumer spans 2024-08-02 17:30:27 +01:00
Eric Allam 1353d66ca1 Additional logging to help track down possible issue with MarQS selection algo 2024-08-02 12:27:50 +01:00
Matt Aitken 1c5ee5d94c Added TaskRun completedAt column (#1243)
* Added TaskRun completedAt column

* When finalising a run set the completedAt date

* A note to discuss whether we need to set the completedAt to null

* Remove the note in the sharedQueueConsumer
2024-08-02 10:31:38 +01:00
nicktrn 4a2ad323ae Release 3.0.0-beta.52
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 4s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-infra (push) Has been skipped
2024-08-01 12:59:14 +01:00
github-actions[bot] a49edd3251 chore: Update version for release (beta) (#1237)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-01 12:57:39 +01:00
nicktrn 8cae1d0875 v3: fix triggering tasks with custom queues (#1242)
* add missing dotenv requires in catalog scripts

* pass task queue options to all task trigger functions

* remove unnecessary include

* fallback to background worker task queue options

* add changeset
2024-08-01 11:26:34 +01:00
Matt Aitken 994ea7c3c8 Update the docs to reflect the new tags limit of 5 (up from 3) 2024-07-31 17:56:45 +01:00
Matt Aitken 09413a62a4 Improved run page and replaying (#1240)
* PropertyTable component changed to use sub components

* Separate run span component

* Early WIP on tabs that use search query

* Shortcut key tabs for the span panel

* The span timeline is working

* When runs get expired, update the OTEL event with an error

* Improved the expired error message

* Reveal env vars when editing

* Tightened things up a bit

* Added detail tab properties

* Progress dashed line

* Move the env label next to the Run number title

* Top level cancel/replay buttons

* Replay with a different payload and environment

* Fix for non json payloads

* Hide the clear/copy buttons

* UI improvements with large payloads

* Close the panels when you replay/cancel

* Added the new timeline to spans

* Use the u-turn left icon for replay

* Added an index for spanId on TaskRun

* Remove replay/cancel buttons the span view

* Replay shortcut works inside the code editor

* Split the log/span inspector between Overview and Detail as well

* More improvements to the inspector

* Context and output improvements

* Focus on run working

* Added version to run.ctx

* Added some padding to the detail view

* Added context tab with shortcut

* Only load the replay data when the dialog is open

* Replaying uses the tags from the original run

* Links are now text links

* Removed version links for now because we don’t have dropdown filters for them yet

* Tabs are now outside of the scrollview

* The inspector is now 30% of the width by default

* Allow replaying and editing SuperJSON payloads

* Deleted unused CodeGroup file

* Increase the tags limit to 5, do the limiting on the server

* The admin tooltip now always shows basic org, project and user info

* Fix for schedule inspector disabled state layout

* Remove new unused span metadata and context

* Removed unused import
2024-07-31 15:11:50 +01:00
Quetzalcoatl c1d4c04e89 add xdg command check for linux servers (#1225)
* add xdg command check for linux

* bump version of cli-v3, add changeset

* Don't bump the version

* lint and log

* prettier log message

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2024-07-30 15:15:06 +01:00
nicktrn 9882d66f87 v3: pre-pull deployments for faster startups from the first run (#1236)
* pre-pull deployed images on all workers

* automatically clean up pre-pull resources

* rename pre-pull image to deployment

* add changeset
2024-07-26 13:12:58 +01:00
nicktrn e27d5cd861 Prevent registry proxy from trying to send responses twice 2024-07-26 08:44:03 +01:00
nicktrn e64cd5ee86 Fix usage page for previous months 2024-07-25 18:34:14 +01:00
Matt Aitken 56060d3f9c Fix for bad package name in docs. @trigger-dev/sdk -> @trigger.dev/sdk/v3 2024-07-24 15:51:32 +01:00
Matt Aitken c0daeffe0c Release 3.0.0-beta.51 2024-07-24 13:59:55 +01:00
github-actions[bot] 53cf5c089d chore: Update version for release (beta) (#1233)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-24 13:55:32 +01:00
James Ritchie ac088c1a73 Fixed safari bug not respecting recharts height 2024-07-24 12:11:01 +01:00
Hamza 83a35226a7 removed CLOUD_ENV from .env.example (#1230) 2024-07-24 12:02:03 +01:00
James Ritchie 12d2bd6cdd Added links to featurebase in the docs and app feedback panel (#1213)
* Added links to the new roadmap and feature request pages. Also redirected the old feature-matrix page to docs home

* Added a new request feature link to the help and feedback panel

* Added Book a call with the founders option in the feedback panel
2024-07-24 11:59:48 +01:00
Matt Aitken 8614ea2107 Tags docs and a couple of minor fixes (#1235)
* runs.list and runs.retrieve OpenAPI docs updated

* Added docs for how to use runs.retrieve and runs.list with usage data

* SDK reference for triggering with tags

* Added Tags docs page to the sidenav, it’s a copy of usage right now

* Fix for type “users” -> “uses”

* Docs for the tags feature

* Fix for bad import

* Added docs how propagating tags to child runs

* Better error messages when Zod parsing fails for triggering and batch triggering

* Tags are now a string or array of strings, this makes it a lot easier to use
2024-07-24 11:22:03 +01:00
Eric Allam 979bee50d0 Fix return type of runs.retrieve, and allow passing the type of the task to runs.retrieve 2024-07-23 23:28:14 +01:00
Eric Allam 086a0f95c5 Extract common trigger code into internal functions and add a tasks.batchTriggerAndWait function 2024-07-23 22:48:59 +01:00
Matt Aitken 8dfd47eb41 Fix for the tags making the run table rows taller 2024-07-23 19:17:02 +01:00
Matt Aitken 55264657d7 Run tags (#1232)
* TaskRunTag migration. Removed unused TaskTag

* Show tags for a run in the run list

* API endpoint for searching tags

* Let’s not expose an API endpoint for tags at the moment

* Filter by tags working from the URL

* Tag async filter working

* We don’t need a “None” option because it’s multi-select

* When triggering a run you can add tags

* SDK runs.retrieve and runs.list with tag support

* Tidied imports

* Changed the run list query so we show all the tags even if a run only matches one of them

* Fix for dealing with weird characters in tags

* Run tags changeset

* Creating a project has a proper loading state (and blocks multiple)

* Improved the error message for tag length

* Added a tags icon for display on the run screen

* Convenient functions for creating and getting run tags

* tags.set() from inside the run function

* Replay a run passes tags through

* Less ridiculous tags for the catalog example

* Allow passing just a string for the tags

* Order by id because there’s an index on the primary key already

* Trim the tags earlier so we don’t accidentally error if a blank string is passed

* Renamed some thing from setTags to addTags

* Use findFirst for the tags project lookup

* Added an index for "TaskRunTag"("name", "id")

It massively improves the performance of run filtering based on tags

* Use `array_agg` for the run list tags so pagination works and we get a single result for each run

* Tidied imports

* More comprehensive test of tags with all triggering functions

* Support tags with tasks.trigger, tasks.batchTrigger and tasks poll variants

* Added tasks.batchTrigger tags

* Sort the tags in the UI so they’re always in the same order

* Added tooltips in the run table for delay, ttl and tags

* Added costInCents, baseCostInCents and durationMs to runs.retrieve and runs.list

* Added support for displaying a split tag if you use key_value or key:value format

* Fix code comment

* Tweaked the JSDoc to make the prefixing clearer

* Added tasks.triggerAndWait to the tags test task
2024-07-23 16:48:09 +01:00
Eric Allam be3157ace7 Don’t yet auto-bundle monorepo dependencies 2024-07-23 12:39:45 +01:00
Émile Ré ae9a8b0ceb Monorepo packages automatic handling (#1208)
* Add fixture

* Update fixture name

* Update fixture to work with npm & pnpm

* Update setup/teardown and README

* Add support for monorepo to e2e suite utilities

* Use direct dependencies info in esbuild plugin

* Renaming

* Add support for yarn

* Add fixture for npm

* Remove console.log call

* Edge cases

* Remove unused options

* Fix error unknown yarn in CI

* Fix logger debug call's error field

* Add changeset

* Fix missing span end
2024-07-23 11:02:48 +01:00
Émile Ré 6e65591e87 Fix cli v3 e2e suite issues (#1207)
* Fix resolve-legacy-peer-deps when PM=pnpm

* Fix setup/teardown

* Fix skip test before installing dependencies

* Re-enable e2e suite in CI

* Add changeset
2024-07-22 16:53:45 +01:00
Matt Aitken 6293290fb9 Better prediction of usage (uses Linear regression) 2024-07-20 10:50:46 +01:00
James Ritchie da592d867a Removed logo 2024-07-19 15:20:40 +01:00
Matt Aitken 24557a7324 Release 3.0.0-beta.50 2024-07-19 11:34:40 +01:00
github-actions[bot] 8b10a592f3 chore: Update version for release (beta) (#1229)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-19 11:33:36 +01:00
Eric Allam d7089e12a9 Replace findUnique with findFirst when doing a relation aggregate 2024-07-19 10:18:56 +01:00
Eric Allam 162d864517 Fixed prisma dataLoader slow query when getting an attempt payload 2024-07-19 09:42:48 +01:00
Matt Aitken 8ba9987944 Declarative schedules (#1226)
* Added type (STATIC or DYNAMIC) to TaskSchedule. Defaults to dynamic

* WIP with dev indexing of static schedules

* Added a code comment

* First stab at deleting unused static schedules

* Dashboard changes for the static schedules

* Generate the description. Upsert the instances when editing. Fix for the friendlyId

* Don’t allow deleting of static schedules

* Don’t allow enabling/disabling of static schedules

* Added filtering for schedule types

* Syncing of schedule for deployed tasks

* Static schedules are now created for each environment

* Added a second static schedule for testing

* Add the type to the schedule task run payload and the object you get back from the SDK

* Changed static/dynamic to declarative/imperative

* Timezone example

* Changeset

* Updated scheduled docs to include declarative

* When you test a schedule it set the type to “IMPERATIVE”

* Improved the tooltip

* Fix for queue time continuing to rise when a run is canceled/expired etc

* Update the info panel on a selected declarative schedule

* Check if there are no instances. This should never happen but log an error if it does

* Throw errors and push them through to the CLI dev command

* Fail deployments if creating the background tasks or schedules fails

* Format the deployment error so it gets displayed

* Changed the maxed out schedules error message to remove bit about support
2024-07-18 20:24:54 +01:00
nicktrn 7056ce53f2 Add missing dep to webapp image
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-infra (push) Has been skipped
2024-07-18 13:58:33 +01:00
nicktrn 2d6fd62fb8 Release 3.0.0-beta.49
🚀 Publish Trigger.dev Docker / units (push) Failing after 5s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 21s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-infra (push) Has been skipped
2024-07-17 14:20:21 +01:00
github-actions[bot] e8993ee882 chore: Update version for release (beta) (#1211)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-17 14:18:40 +01:00
nicktrn e0cdebbe09 Disable excessive websocket heartbeat logs 2024-07-17 13:28:40 +01:00
Matt Aitken d6c6dc993d try/catch opening the v3 CLI login URL. Fix for Ubuntu 2024-07-17 13:15:42 +01:00
nicktrn 27de884f93 Remove unused ping message schema 2024-07-17 13:01:43 +01:00
nicktrn c2707800aa Improve prisma errors for missing generate step 2024-07-17 09:53:38 +01:00
nicktrn dbda820a71 v3: fix websocket timeouts (#1220)
* prevent uncaught websocket exceptions

* dev cli sends regular pings to keep connection alive

* add changeset

* fix for prod message forwarding

* add server-side websocket ping

* Revert "dev cli sends regular pings to keep connection alive"

This reverts commit 9a6a7a21433b9f0665f72be015d0ba5a8d83eba4.

* add dev cli websocket debug logs, always print errors

* actually start the ping service

* update changeset

* catch remaining dev cli message handler errors
2024-07-17 09:05:15 +01:00
Gautam Singh e417aca879 v3: add extraCACerts config (#1214)
* Added extraCACerts config to help with self signed/private ca cert chain

* changeset

* Update changeset

* resolved pr comments

---------

Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2024-07-17 08:56:52 +01:00
Ray Amjad 5a3b4a450b Update github-actions.mdx (#1222) 2024-07-17 08:55:14 +01:00
Matt Aitken f91a118958 Fix for dev env vars getting deleted when another user edits theirs 2024-07-16 10:57:35 +01:00
nicktrn 9971de6a1e Increase span attribute value length limit to 2048 2024-07-16 08:15:05 +01:00
Matt Aitken 0847bee5b8 Fix for stripping out TRIGGER_ env vars 2024-07-15 17:11:15 +01:00
Matt Aitken 6152b2f3dc Don’t error when setting TRIGGER_SECRET_KEY or TRIGGER_API_URL env vars, just filter them out 2024-07-15 16:39:13 +01:00
Matt Aitken ec8381e2d0 Fix for staging environment variable API endpoints
Ugly but effective fix for the fact we can pass the slug in as “staging” when we use “stg” internally
2024-07-15 16:33:00 +01:00
Matt Aitken 76a15fd278 The run filter for tasks query was very slow if you had a lot of synced tasks 2024-07-15 15:59:11 +01:00
nicktrn 824a6205f7 Fix deployments stuck in deploying state 2024-07-15 12:00:16 +01:00
Matt Aitken a0984d5292 Allow alerts from external and unlinked Slack channels 2024-07-12 11:14:57 +01:00
Matt Aitken c61d8ed890 Removed badge from the Tasks in the side menu
It was showing the v2 job count. Mostly this didn't actually show anything but one user reported they saw a number here and it's definitely a bug.
2024-07-12 10:51:54 +01:00
Eric Allam 76a0f61dee v3: improve schedule reliability by ensuring next tick is always scheduled 2024-07-11 15:35:34 +01:00
Jacob Paris d934feb02a feature: add no-trigger-core-import (#1175)
* feature: add no-trigger-core-import

* fix: handle aliases and limit to exported modules

* fix: use longest export map name

* fix: support export type and interface

* fix: simplify message when export map is needed

* feature: fine grained modules for core

* fix: add prerequisite lint plugins

* fix: why do I have to fix these components again???

* fix: tests

* Add eslint config and vscode settings

* Create yellow-roses-arrive.md

* Remove the vscode linting

This is temporary so no one fixes these until we merge the remix branch

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
Co-authored-by: Eric Allam <eallam@icloud.com>
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-07-11 15:14:55 +01:00
Eric Allam 7fdedf3137 Fix excessive logs from the MarQS rebalancer 2024-07-11 14:20:59 +01:00
Eric Allam f6e23e94d1 Ensure failed migrations fail deployment by not swallowing exit codes in entrypoint.sh 2024-07-11 13:22:40 +01:00
Matt Aitken 3d6cfe5ead Admin API endpoint that adds v3 staging environments, and updates their concurrency 2024-07-11 10:22:21 +01:00
nicktrn 946efd1fb4 v3: deployment index retry (#1205)
* add built at date to worker deployments

* stay on correct deployment page when clicking row

* expose built at via presenters

* more specific deployment statuses

* add deployment indexing retry

* improve deploy timeout logs and add dequeue on completion

* unify deployment indexing retryable status

* only allow index retries of the latest deployment

* improve jsxfu
2024-07-10 17:54:36 +01:00
Matt Aitken 3d645e2b2c Fix for usage of zero displaying weirdly 2024-07-10 17:31:40 +01:00
Matt Aitken 45cb75b8a3 Only show dev runs in the tasks usage table 2024-07-10 17:22:35 +01:00
Matt Aitken 91d45e5ba6 Added a link to the pricing page for machines 2024-07-10 17:04:29 +01:00
James Ritchie fa5b3fe11e Only show the github verified badge on the Select Plan onboarding route 2024-07-10 16:50:52 +01:00
James Ritchie 90506322b5 Hide the tooltip content on the usage graph 2024-07-10 16:46:10 +01:00
Matt Aitken e59dd465d8 Documented the task usage functions 2024-07-10 15:19:45 +01:00
Matt Aitken 98d5f005e4 Added delay to the docs and improved the ttl 2024-07-10 10:28:49 +01:00
Matt Aitken a6b5d2edc5 Added ttl to trigger docs 2024-07-10 09:41:09 +01:00
Matt Aitken f4a1d5f36d Removed broken links from writing tasks page 2024-07-10 06:35:41 +01:00
Matt Aitken 6fb6cb6aaf Docs: added alerts link from feature matrix 2024-07-10 06:35:41 +01:00
Eric Allam 0b6417f1d8 Improve the section on adding trigger.dev to dev deps 2024-07-09 21:37:37 +01:00
Matt Aitken a685f5a4ec Removed the enable/disable v3 from the admin area 2024-07-09 19:00:19 +01:00
Matt Aitken 62532d5ce1 Fix for package name mistake 2024-07-09 15:53:03 +01:00
Matt Aitken 786d5508f0 Cover upgrading dev dependency 2024-07-09 15:52:42 +01:00
Matt Aitken 187f02780b Added upgrading packages guide 2024-07-09 15:49:46 +01:00
Eric Allam b8b3ea07eb Release 3.0.0-beta.48
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-infra (push) Has been skipped
2024-07-09 14:06:41 +01:00
github-actions[bot] e341df806c chore: Update version for release (beta) (#1209)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-09 14:05:49 +01:00
Eric Allam ecf1110ab7 v3: Export AbortTaskRunError from @trigger.dev/sdk/v3 2024-07-09 14:00:44 +01:00
Eric Allam 9ed97b3996 Release 3.0.0-beta.47 2024-07-09 12:25:02 +01:00
github-actions[bot] 2f14e5136d chore: Update version for release (beta) (#1203)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-09 12:24:09 +01:00
nicktrn 1249523922 Switch from latest to v3 tag for versioned container images 2024-07-09 10:27:35 +01:00
Matt Aitken 9ba5dbaf91 When manipulating dates for periods force UTC 2024-07-09 09:25:08 +01:00
Eric Allam 16ad595338 v3: update @depot/cli to latest 0.0.1-cli.2.71.0 2024-07-08 19:33:15 +01:00
Matt Aitken 681bb729e9 Added GitHub verification failed route 2024-07-08 18:48:39 +01:00
Matt Aitken 07fb3fd90d When replaying and they’re out of credits return undefined so we can deal with it 2024-07-08 18:48:36 +01:00
Matt Aitken 3bfebc06cf When running a test and they’ve run out of credits, show an error message 2024-07-08 18:48:12 +01:00
Matt Aitken 30f7866e17 Send different error if they’ve run out of credits 2024-07-08 18:47:52 +01:00
Eric Allam 46789292b5 Add back in the run link when calling trigger 2024-07-08 17:59:48 +01:00
Eric Allam e04d44866f v3: better handle null unicode characters when creating task events and completing a failed attempt 2024-07-08 15:59:19 +01:00
Matt Aitken d48d9a0e00 Removed invite-only from readme 2024-07-08 15:23:27 +01:00
Matt Aitken 72f3a4b128 Usage and billing (v3) (#1201)
* v3 subscription endpoints

* Use pnpm linked billing package during development

* Moved v2 billing components into a subfolder

* Select plan using the real data

* Improved v3 plan display

* Use new api response that doesn’t require a Stripe call

* Free flow is working

* Added GitHub modal and verified badge

* Deleted old request v3 access component/route

* Allow setting classes on the Tooltip button

* Paid plans working

* Redirect from select plan if you’ve got v3 enabled

* Loading state improvements

* New admin API endpoint to set concurrency across multiple environments

* When projects are created, conditionally create staging based on the plan

* New billing page working with side menu and stripe portal

* Layout, formatting and some plan state improvements/fixes

* Don’t show the period if you’re on the free plan

* Temporary upgrade callout

* Refactored the platform code so it’s easier to call and doesn’t require a isManagedCloud check

* Send taskIdentifier to OpenMeter

* Side menu

* Upgrade prompts

* More improvements to the app-wide usage indicators

* Early work on usage graphs

* Added the usage bar for v3

* Moved code to presenter and now using defer

* Added the tasks table to usage

* Improved the v3 usage bar if theres’ no usage on a paid plan

* If no run data, still render a graph

* Usage page errors when defered loading fails

* Don’t show the public API key for v3, they’re not used and probably never will be

* Improved the upgrade callout and API keys page layout

* Only show the “reveal all” toggle if you have environment variables in the table

* Replaced Upgrade callout with a more generic InfoPanel component

* better panel width

* Show conditional upgrade prompts based on plan and number of schedules used

* Removed duplicate class

* Wider blank state panels for the scheduled page

* Wider info panel for the env var page

* Blank state now using the info panel

* Platform alerts prompt now using the InfoPanel

* Deploy blank state uses InfoPanel

* Github verified badge padding adjustment

* Improved the layout of the page, some style tweaks, organized imports

* Better default tooltip style

* could be undefined fix

* text fix + style updates

* Changed the billing icon in the side menu

* Billing page layout and style improvements

* plan tooltips don’t use dark variant

* Don’t highlight the plan on the billing page

* Tooltip underlines stand out more

* Fixed padding in the PageTitle

* Tooltips use the correct cursor

* Improved the plan banner on the billing page

* Fixed Header1 inconsistent font weight

* Fixed issue where input field focus states were being clipped

* Fixed large button not having large text size

* Added a link to the Get in touch copy and improved the connect to GitHub modal

* Select plan page uses the MainCenteredContainer

* Better logging from the Loops endpoint because this error finally got hit

* Move the ingestion of compute to the platform

* Reporting usage of invocations moved to the platform

* Get the entitlement before triggering a non-dev task

* Contact us enterprise plan button opens the feedback form

* Removed Github discussions link from the Feedback panel

* Swapped billing icon for credit card

* Show a Unlock staging panel on the env var page

* Updated staging environment colour

* Show a prompt to upgrade to get staging in the new env var modal

* Improved the edit env var modal

* Implement ability to disable org concurrency

* Use common logic for the plans

* Use the billing server to get the schedule limits

* Some schedules page fixes

* More convenient way of getting a limit

* Use the new schedule limit

* Team member limiting

* Made the limit visible on the team page

* Limit alerts

* Added an index for TaskRun.scheduleId

* Remove console.log on schedules page

* Added durations to the run table

* Tabular numbers

* Improved the usage page formatting

* Only admins see the compute column on the run table

* Include the base cost on the usage stats

* Moved the status to the sidebar

* Optional table header tooltip

* Allow InfoIconTooltips to have customizable content styles

* Added a tooltip to the duration header, changed no test to a dash

* Removed all references to signing up to v3 from the docs

* Switched @trigger.dev/billing to @trigger.dev/platform

* Passing up the variant for the InfoIconTooltip

* table tooltip max-width fixed

* Switch to the published @trigger.dev/platform 1.0.11

* v2 usage page title changed to include “v2"

* code theme has a transparent background so it works on any background

* duration columns now grouped together nicely at wide screen size

* Last duration column fills the width properly

* Fix for the per run price being in cents not dollars

* Show the total cost with 8 decimal places

* Show 8 decimal places in the usage graph tooltip

* Moved the UpgradePrompt to the v3 folder

* Prepare to use Shadcns chart helpers

* Much nicer chart

* Small tweaks to the graph

* Fix run table col spans for empty/loading messages

* We don’t need isManagedCloud in createProject

* Hide v3 usage/billing pages if there aren’t v3 projects in your org

* Removed unused tooltipStyle

* Usage bar now says “Included usage” instead of “Tier limit” if you’re paying

* Get the plan/usage data in parallel

* The usage page now has a month dropdown and all data is for that calendar month

* Ensure the passed date is the 1st of the month

* Use the machine presets from the platform package

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
Co-authored-by: Eric Allam <eallam@icloud.com>
2024-07-08 14:42:19 +01:00
Eric Allam 305a6cebd4 Add tasks.triggerAndWait SDK function 2024-07-08 11:48:33 +01:00
James Ritchie c9ac6ef93f Made the bulk action bar taller, bigger buttons and more colour. (#1202) 2024-07-05 19:19:29 +01:00
Dan d2d54c9bc4 v3 readme update (#1197)
* v3 readme update

* Added self-hosting link
2024-07-05 10:35:28 +01:00
Dan a440101af4 Updated the codeblock colours (#1195) 2024-07-05 10:34:56 +01:00
Eric Allam 4f95c9de4e v3: Cancel awaited subtasks and reliable rate-limit recovery (#1200)
* v3: cancel subtasks when parent task runs are cancelled

* v3: recover from server rate limiting errors in a more reliable way

- Changing from sliding window to token bucket in the API rate limiter, to help smooth out traffic
- Adding spans to the API Client core & SDK functions
- Added waiting spans when retrying in the API Client
- Retrying in the API Client now respects the x-ratelimit-reset
- Retrying ApiError’s in tasks now respects the x-ratelimit-reset
- Added AbortTaskRunError that when thrown will stop retries
- Added idempotency keys SDK functions and automatically injecting the run ID when inside a task
- Added the ability to configure ApiRequestOptions (retries only for now) globally and on specific calls
- Implement the maxAttempts TaskRunOption (it wasn’t doing anything before)

* Adding some docs about the request options

* Fix type error

* Remove context propagation through graphile jobs

* Remove logger

* only select a subset of task run columns

* limit columns selected in batchTrigger as well

* added idempotency doc

* allow scoped idempotency keys, and fixed an issue with the unique index on BatchTaskRun and TaskRun

* Removed old cancel task run children code
2024-07-05 10:30:52 +01:00
896 changed files with 25211 additions and 29930 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Increase span attribute value length limit to 2048
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/core-apps": patch
"@trigger.dev/core": patch
---
Pre-pull deployment images for faster startups
+7
View File
@@ -0,0 +1,7 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---
- Prevent uncaught exceptions when handling WebSocket messages
- Improve CLI dev command WebSocket debug and error logging
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Fixes for continuing after waits
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Added version to ctx.run
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Fix trigger functions for custom queues
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Improve prisma errors for missing postinstall
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Fix return type of runs.retrieve, and allow passing the type of the task to runs.retrieve
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
try/catch opening the login URL
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Fix automatic opening of login URL on linux-server systems with missing xdg-open
+12
View File
@@ -0,0 +1,12 @@
---
"@trigger.dev/core-apps": patch
"trigger.dev": patch
"@trigger.dev/core": patch
---
Fix issues that could result in unreezable state run crashes. Details:
- Never checkpoint between attempts
- Some messages and socket data now include attempt numbers
- Remove attempt completion replays
- Additional prod entry point logging
- Fail runs that receive deprecated (pre-lazy attempt) execute messages
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Extract common trigger code into internal functions and add a tasks.batchTriggerAndWait function
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
v3: recover from server rate limiting errors in a more reliable way
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Rollback to try and fix some dependent attempt issues
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
v3: update @depot/cli to latest 0.0.1-cli.2.71.0
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Automatically bundle internal packages that use file, link or workspace protocl
+6
View File
@@ -0,0 +1,6 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---
Added config option extraCACerts to ProjectConfig type. This copies the ca file along with additionalFiles and sets NODE_EXTRA_CA_CERTS environment variable in built image as well as running the task.
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
v3: Export AbortTaskRunError from @trigger.dev/sdk/v3
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
You can now add tags to runs and list runs using them
+23
View File
@@ -46,11 +46,13 @@
"changesets": [
"afraid-sheep-joke",
"angry-eagles-trade",
"angry-trees-drop",
"beige-pears-explode",
"beige-pens-dance",
"big-tomatoes-deliver",
"blue-pumas-whisper",
"breezy-gorillas-mate",
"brown-boats-bathe",
"brown-spies-burn",
"chilled-hornets-move",
"clean-pianos-listen",
@@ -60,34 +62,48 @@
"cool-comics-burn",
"cool-glasses-bake",
"cuddly-feet-approve",
"curly-monkeys-tell",
"dry-walls-check",
"dull-mangos-press",
"early-impalas-itch",
"eight-pumas-float",
"eleven-paws-join",
"famous-boats-tease",
"fast-colts-relax",
"fast-melons-listen",
"few-poems-vanish",
"few-students-share",
"fifty-lions-think",
"five-toes-destroy",
"flat-onions-punch",
"friendly-walls-repair",
"funny-swans-destroy",
"gorgeous-cycles-guess",
"gorgeous-gorillas-compete",
"green-bags-wink",
"green-pens-battle",
"hot-buckets-behave",
"hot-fishes-retire",
"hot-wasps-sin",
"hungry-sloths-promise",
"itchy-chairs-itch",
"khaki-apricots-design",
"khaki-poems-lay",
"large-seahorses-cheat",
"late-icons-lie",
"late-steaks-behave",
"lazy-files-lay",
"lemon-jobs-repair",
"lemon-sloths-hide",
"light-bulldogs-press",
"light-dragons-complain",
"little-crabs-cross",
"long-feet-invent",
"long-fireants-search",
"long-hounds-wave",
"loud-actors-remember",
"lovely-drinks-flash",
"lucky-items-film",
"many-ligers-pump",
"mighty-camels-joke",
"mighty-eggs-grab",
@@ -95,11 +111,14 @@
"mighty-parrots-sin",
"modern-stingrays-end",
"nasty-jars-pump",
"nervous-baboons-sin",
"nervous-planets-sparkle",
"nervous-seas-shave",
"new-pants-beg",
"new-rivers-tell",
"nice-bulldogs-turn",
"ninety-pets-travel",
"odd-beds-wonder",
"odd-poets-own",
"pink-pumas-rhyme",
"plenty-ducks-beam",
@@ -120,6 +139,7 @@
"selfish-ducks-sort",
"serious-hats-rest",
"shaggy-spoons-taste",
"shaggy-weeks-live",
"sharp-emus-compare",
"sharp-zebras-serve",
"shiny-coats-cry",
@@ -158,6 +178,7 @@
"tender-turkeys-compete",
"thick-carrots-sneeze",
"thin-parents-heal",
"thirty-hotels-raise",
"thirty-islands-kiss",
"tidy-balloons-suffer",
"tidy-dryers-sleep",
@@ -172,6 +193,8 @@
"violet-clocks-notice",
"warm-olives-provide",
"warm-planes-taste",
"yellow-roses-arrive",
"young-jars-wait",
"young-snails-sell"
]
}
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
v3: sanitize errors with null unicode characters in some places
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
Added declarative cron schedules
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Add more package exports that can be used from the web app
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Fix various e2e issues for 'resolve-legacy-peer-deps' fixture, installation of fixture deps and lockfile-based test skipping'
+1 -2
View File
@@ -11,7 +11,6 @@ REMIX_APP_PORT=3030
APP_ENV=development
APP_ORIGIN=http://localhost:3030
NODE_ENV=development
CLOUD_ENV=development
V3_ENABLED=true
# Redis is used for the v3 queuing and v2 concurrency control
@@ -79,4 +78,4 @@ COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl
# INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADER_VALUE=<Auth token value>
# INTERNAL_OTEL_TRACE_LOGGING_ENABLED=1
# INTERNAL_OTEL_TRACE_SAMPING_RATE=20 # this means 1/20 traces or 5% of traces will be sampled (sampled = recorded)
# INTERNAL_OTEL_TRACE_INSTRUMENT_PRISMA_ENABLED=0,
# INTERNAL_OTEL_TRACE_INSTRUMENT_PRISMA_ENABLED=0,
+3
View File
@@ -42,6 +42,9 @@ jobs:
- name: 🔧 Build worker template files
run: pnpm --filter trigger.dev run build:workers
- name: Enable corepack
run: corepack enable
- name: Run E2E Tests
run: |
PM=${{ matrix.package-manager }} pnpm --filter trigger.dev run test:e2e
+2 -2
View File
@@ -51,9 +51,9 @@ jobs:
ref_without_tag=ghcr.io/triggerdotdev/trigger.dev
image_tags=$ref_without_tag:${{ steps.get_version.outputs.version }}
# if it's a versioned tag, also tag it as latest
# if it's a versioned tag, also tag it as v3
if [[ "${{ github.ref_name }}" == v.docker.* ]]; then
image_tags=$image_tags,$ref_without_tag:latest
image_tags=$image_tags,$ref_without_tag:v3
fi
echo "IMAGE_TAGS=${image_tags}" >> "$GITHUB_OUTPUT"
+3 -3
View File
@@ -111,11 +111,11 @@ jobs:
REPOSITORY: ${{ steps.prep.outputs.REPOSITORY }}
IMAGE_TAG: ${{ steps.prep.outputs.IMAGE_TAG }}
- name: 🐙 Push 'latest' to GitHub Container Registry
- name: 🐙 Push 'v3' tag to GitHub Container Registry
if: startsWith(github.ref_name, 'v.docker.')
run: |
docker tag infra_image $REGISTRY/$REPOSITORY:latest
docker push $REGISTRY/$REPOSITORY:latest
docker tag infra_image $REGISTRY/$REPOSITORY:v3
docker push $REGISTRY/$REPOSITORY:v3
env:
REGISTRY: ghcr.io/triggerdotdev
REPOSITORY: ${{ steps.prep.outputs.REPOSITORY }}
+5 -5
View File
@@ -49,11 +49,11 @@ jobs:
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
# e2e:
# uses: ./.github/workflows/e2e.yml
# with:
# package: cli-v3
# secrets: inherit
e2e:
uses: ./.github/workflows/e2e.yml
with:
package: cli-v3
secrets: inherit
publish:
needs: [typecheck, units]
+32 -25
View File
@@ -5,64 +5,71 @@
<img alt="Trigger.dev logo" src="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/a45d1fa2-0ae8-4a39-4409-f4f934bfae00/public">
</picture>
### The open source background jobs platform
### Open source background jobs with no timeouts
[Discord](https://trigger.dev/discord) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/triggerdotdev.svg?style=social&label=Follow%20%40trigger.dev)](https://twitter.com/triggerdotdev)
[![GitHub Repo stars](https://img.shields.io/github/stars/triggerdotdev/trigger.dev?style=social)](https://github.com/triggerdotdev/trigger.dev)
</div>
> The Trigger.dev v3 developer preview is now open. For more information and to get early access, check out our [developer preview launch post](https://trigger.dev/blog/v3-developer-preview-launch/).
## About Trigger.dev
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
Trigger.dev is an open source platform and SDK which allows you to create long-running background jobs with no timeouts. Write normal async code, deploy, and never hit a timeout.
## Long running Jobs on serverless
#### Features:
Reliably run jobs and dont worry about function timeouts, we handle those for you.
- Auto-resume after a function timeout
- Auto-resume after a server outage
- Add delays of up to a year
- JavaScript and TypeScript SDK
- Write reliable code by default
- No infrastructure to manage
- Works with your existing tech stack
## In your codebase
Create Jobs where they belong: in your codebase. Version control, localhost, test, review, and deploy like you're already used to.
Create tasks where they belong: in your codebase. Version control, localhost, test and review like you're already used to.
## Secure by design
```ts
import { task } from "@trigger.dev/sdk/v3";
We only receive Triggers and the data you choose to send to us. You can even completely self-host the entire platform.
//1. You need to export each task
export const helloWorld = task({
//2. Use a unique id for each task
id: "hello-world",
//3. The run function is the main function of the task
run: async (payload: { message: string }) => {
//4. You can write code that runs for a long time here, there are no timeouts
console.log(payload.message);
},
});
```
## Don't worry about deployment
## Deployment
Just use our SDK to write Jobs in your codebase. There's nothing extra to deploy and no CI to configure, your Jobs just connect to our cloud. Or you can always self-host.
Use our SDK to write tasks in your codebase. There's no infrastructure to manage, your tasks automatically scale and connect to our cloud. Or you can always [self-host](https://trigger.dev/docs/v3/open-source-self-hosting#overview).
## Environments
We support `Development`, `Staging`, and `Production` environments, allowing you to test your tasks before deploying them to production.
## Full visibility of every job run
View every Task in every Run so you can tell exactly what happened.
View every task in every run so you can tell exactly what happened. We provide a full trace view of every task run so you can see what happened at every step.
![image](https://www.trigger.dev/build/_assets/web-app-2QFKXFLW.png)
## Built-in integrations
Easily integrate with hundreds of third-party APIs including your own. Use API keys (which never leave your server) or let us handle OAuth for you. Install our integration packages and easily subscribe to webhooks and perform common tasks, or you can easily use your existing favorite Node.JS SDKs and get resumability and idempotency through our `runTask` function.
![Trace view image](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/7c1b347f-004c-4482-38a7-3f6fa9c00d00/public)
# Getting started
Visit our docs [here](https://trigger.dev/docs).
Visit our docs [here](https://trigger.dev/docs/v3/introduction) for a full guide on how to get started with Trigger.dev.
## Self-host
We provide an official trigger.dev docker image you can use to easily self-host the platform. We're working on more extensive guides but we currently provide a [Fly.io example repository](https://github.com/triggerdotdev/fly.io) with instructions in the README for deploying and using a self-hosted instance of Trigger.dev on Fly.io.
If you prefer to self-host, you can follow our [self-hosting guide](https://trigger.dev/docs/v3/open-source-self-hosting#overview).
## Development
To setup and develop locally or contribute to the open source project, follow our [development guide](./CONTRIBUTING.md).
## Meet the Amazing People Behind This Project 🚀
## Meet the Amazing People Behind This Project:
<a href="https://github.com/triggerdotdev/trigger.dev/graphs/contributors">
<img src="https://contrib.rocks/image?repo=triggerdotdev/trigger.dev" />
+7 -3
View File
@@ -12,7 +12,11 @@ export class ChaosMonkey {
private chaosEventRate = 0.2;
private delayInSeconds = 45;
constructor(private enabled = false) {
constructor(
private enabled = false,
private disableErrors = false,
private disableDelays = false
) {
if (this.enabled) {
console.log("🍌 Chaos monkey enabled");
}
@@ -32,8 +36,8 @@ export class ChaosMonkey {
async call({
$,
throwErrors = true,
addDelays = true,
throwErrors = !this.disableErrors,
addDelays = !this.disableDelays,
}: {
$?: Execa$<string>;
throwErrors?: boolean;
+11 -5
View File
@@ -17,6 +17,7 @@ type CheckpointAndPushOptions = {
projectRef: string;
deploymentVersion: string;
shouldHeartbeat?: boolean;
attemptNumber?: number;
};
type CheckpointAndPushResult =
@@ -258,6 +259,7 @@ export class Checkpointer {
leaveRunning = true, // This mirrors kubernetes behaviour more accurately
projectRef,
deploymentVersion,
attemptNumber,
}: CheckpointAndPushOptions): Promise<CheckpointAndPushResult> {
this.#logger.log("Checkpointing with backoff", {
runId,
@@ -297,6 +299,7 @@ export class Checkpointer {
leaveRunning,
projectRef,
deploymentVersion,
attemptNumber,
});
if (result.success) {
@@ -359,6 +362,7 @@ export class Checkpointer {
leaveRunning = true, // This mirrors kubernetes behaviour more accurately
projectRef,
deploymentVersion,
attemptNumber,
}: CheckpointAndPushOptions): Promise<CheckpointAndPushResult> {
await this.init();
@@ -367,6 +371,7 @@ export class Checkpointer {
leaveRunning,
projectRef,
deploymentVersion,
attemptNumber,
};
if (!this.#dockerMode && !this.#canCheckpoint) {
@@ -418,13 +423,14 @@ export class Checkpointer {
this.#logger.log("Checkpointing:", { options });
const containterName = this.#getRunContainerName(runId);
const containterNameWithAttempt = this.#getRunContainerName(runId, attemptNumber);
// Create checkpoint (docker)
if (this.#dockerMode) {
try {
if (this.opts.forceSimulate || !this.#canCheckpoint) {
this.#logger.log("Simulating checkpoint");
this.#logger.debug(await $$`docker pause ${containterName}`);
this.#logger.debug(await $$`docker pause ${containterNameWithAttempt}`);
} else {
if (this.simulateCheckpointFailure) {
if (performance.now() < this.simulateCheckpointFailureSeconds * 1000) {
@@ -435,11 +441,11 @@ export class Checkpointer {
if (leaveRunning) {
this.#logger.debug(
await $$`docker checkpoint create --leave-running ${containterName} ${exportLocation}`
await $$`docker checkpoint create --leave-running ${containterNameWithAttempt} ${exportLocation}`
);
} else {
this.#logger.debug(
await $$`docker checkpoint create ${containterName} ${exportLocation}`
await $$`docker checkpoint create ${containterNameWithAttempt} ${exportLocation}`
);
}
}
@@ -581,7 +587,7 @@ export class Checkpointer {
return this.#failedCheckpoints.has(runId);
}
#getRunContainerName(suffix: string) {
return `task-run-${suffix}`;
#getRunContainerName(suffix: string, attemptNumber?: number) {
return `task-run-${suffix}${attemptNumber && attemptNumber > 1 ? `-att${attemptNumber}` : ""}`;
}
}
+43 -4
View File
@@ -49,7 +49,11 @@ const PLATFORM_SECRET = process.env.PLATFORM_SECRET || "coordinator-secret";
const SECURE_CONNECTION = ["1", "true"].includes(process.env.SECURE_CONNECTION ?? "false");
const logger = new SimpleLogger(`[${NODE_NAME}]`);
const chaosMonkey = new ChaosMonkey(!!process.env.CHAOS_MONKEY_ENABLED);
const chaosMonkey = new ChaosMonkey(
!!process.env.CHAOS_MONKEY_ENABLED,
!!process.env.CHAOS_MONKEY_DISABLE_ERRORS,
!!process.env.CHAOS_MONKEY_DISABLE_DELAYS
);
class TaskCoordinator {
#httpServer: ReturnType<typeof createServer>;
@@ -290,6 +294,7 @@ class TaskCoordinator {
setSocketDataFromHeader("projectRef", "x-trigger-project-ref");
setSocketDataFromHeader("runId", "x-trigger-run-id");
setSocketDataFromHeader("attemptFriendlyId", "x-trigger-attempt-friendly-id", false);
setSocketDataFromHeader("attemptNumber", "x-trigger-attempt-number", false);
setSocketDataFromHeader("envId", "x-trigger-env-id");
setSocketDataFromHeader("deploymentId", "x-trigger-deployment-id");
setSocketDataFromHeader("deploymentVersion", "x-trigger-deployment-version");
@@ -306,6 +311,10 @@ class TaskCoordinator {
onConnection: async (socket, handler, sender) => {
const logger = new SimpleLogger(`[prod-worker][${socket.id}]`);
const getAttemptNumber = () => {
return socket.data.attemptNumber ? parseInt(socket.data.attemptNumber) : undefined;
};
const crashRun = async (error: { name: string; message: string; stack?: string }) => {
try {
this.#platformSocket?.send("RUN_CRASHED", {
@@ -381,6 +390,10 @@ class TaskCoordinator {
socket.data.attemptFriendlyId = attemptFriendlyId;
};
const updateAttemptNumber = (attemptNumber: string | number) => {
socket.data.attemptNumber = String(attemptNumber);
};
this.#platformSocket?.send("LOG", {
metadata: socket.data,
text: "connected",
@@ -430,6 +443,7 @@ class TaskCoordinator {
});
updateAttemptFriendlyId(executionAck.payload.execution.attempt.id);
updateAttemptNumber(executionAck.payload.execution.attempt.number);
} catch (error) {
logger.error("Error", { error });
@@ -505,11 +519,17 @@ class TaskCoordinator {
updateAttemptFriendlyId(message.attemptFriendlyId);
this.#platformSocket?.send("READY_FOR_RESUME", message);
if (message.version === "v2") {
updateAttemptNumber(message.attemptNumber);
}
this.#platformSocket?.send("READY_FOR_RESUME", { ...message, version: "v1" });
});
// MARK: RUN COMPLETED
socket.on("TASK_RUN_COMPLETED", async ({ completion, execution }, callback) => {
socket.on("TASK_RUN_COMPLETED", async (message, callback) => {
const { completion, execution } = message;
logger.log("completed task", { completionId: completion.id });
// Cancel all in-progress checkpoints (if any)
@@ -518,8 +538,10 @@ class TaskCoordinator {
await chaosMonkey.call({ throwErrors: false });
const completeWithoutCheckpoint = (shouldExit: boolean) => {
const supportsRetryCheckpoints = message.version === "v1";
this.#platformSocket?.send("TASK_RUN_COMPLETED", {
version: "v1",
version: supportsRetryCheckpoints ? "v1" : "v2",
execution,
completion,
});
@@ -546,6 +568,15 @@ class TaskCoordinator {
if (completion.retry.delay < this.#delayThresholdInMs) {
completeWithoutCheckpoint(false);
// Prevents runs that fail fast from never sending a heartbeat
this.#sendRunHeartbeat(socket.data.runId);
return;
}
if (message.version === "v2") {
completeWithoutCheckpoint(true);
return;
}
@@ -681,6 +712,7 @@ class TaskCoordinator {
runId: socket.data.runId,
projectRef: socket.data.projectRef,
deploymentVersion: socket.data.deploymentVersion,
attemptNumber: getAttemptNumber(),
});
if (!checkpoint) {
@@ -752,6 +784,7 @@ class TaskCoordinator {
runId: socket.data.runId,
projectRef: socket.data.projectRef,
deploymentVersion: socket.data.deploymentVersion,
attemptNumber: getAttemptNumber(),
});
if (!checkpoint) {
@@ -821,6 +854,7 @@ class TaskCoordinator {
runId: socket.data.runId,
projectRef: socket.data.projectRef,
deploymentVersion: socket.data.deploymentVersion,
attemptNumber: getAttemptNumber(),
});
if (!checkpoint) {
@@ -905,6 +939,7 @@ class TaskCoordinator {
}
updateAttemptFriendlyId(createAttempt.executionPayload.execution.attempt.id);
updateAttemptNumber(createAttempt.executionPayload.execution.attempt.number);
callback({
success: true,
@@ -924,6 +959,10 @@ class TaskCoordinator {
if (message.attemptFriendlyId) {
updateAttemptFriendlyId(message.attemptFriendlyId);
}
if (message.attemptNumber) {
updateAttemptNumber(message.attemptNumber);
}
});
},
onDisconnect: async (socket, handler, sender, logger) => {
+4 -4
View File
@@ -109,7 +109,7 @@ class DockerTaskOperations implements TaskOperations {
async create(opts: TaskOperationsCreateOptions) {
await this.init();
const containerName = this.#getRunContainerName(opts.runId);
const containerName = this.#getRunContainerName(opts.runId, opts.nextAttemptNumber);
const runArgs = [
"run",
@@ -150,7 +150,7 @@ class DockerTaskOperations implements TaskOperations {
async restore(opts: TaskOperationsRestoreOptions) {
await this.init();
const containerName = this.#getRunContainerName(opts.runId);
const containerName = this.#getRunContainerName(opts.runId, opts.attemptNumber);
if (!this.#canCheckpoint || this.opts.forceSimulate) {
logger.log("Simulating restore");
@@ -195,8 +195,8 @@ class DockerTaskOperations implements TaskOperations {
return `task-index-${suffix}`;
}
#getRunContainerName(suffix: string) {
return `task-run-${suffix}`;
#getRunContainerName(suffix: string, attemptNumber?: number) {
return `task-run-${suffix}${attemptNumber && attemptNumber > 1 ? `-att${attemptNumber}` : ""}`;
}
async #sendPostStart(containerName: string): Promise<void> {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2016",
"target": "es2018",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
+98 -7
View File
@@ -4,6 +4,7 @@ import {
TaskOperations,
TaskOperationsCreateOptions,
TaskOperationsIndexOptions,
TaskOperationsPrePullDeploymentOptions,
TaskOperationsRestoreOptions,
} from "@trigger.dev/core-apps/provider";
import { SimpleLogger } from "@trigger.dev/core-apps/logger";
@@ -49,6 +50,7 @@ class KubernetesTaskOperations implements TaskOperations {
#k8sApi: {
core: k8s.CoreV1Api;
batch: k8s.BatchV1Api;
apps: k8s.AppsV1Api;
};
constructor(namespace = "default") {
@@ -100,7 +102,7 @@ class KubernetesTaskOperations implements TaskOperations {
resources: {
limits: {
cpu: "1",
memory: "1G",
memory: "2G",
"ephemeral-storage": "2Gi",
},
},
@@ -137,10 +139,12 @@ class KubernetesTaskOperations implements TaskOperations {
}
async create(opts: TaskOperationsCreateOptions) {
const containerName = this.#getRunContainerName(opts.runId, opts.nextAttemptNumber);
await this.#createPod(
{
metadata: {
name: this.#getRunContainerName(opts.runId),
name: containerName,
namespace: this.#namespace.metadata.name,
labels: {
...this.#getSharedLabels(opts),
@@ -155,7 +159,7 @@ class KubernetesTaskOperations implements TaskOperations {
terminationGracePeriodSeconds: 60 * 60,
containers: [
{
name: this.#getRunContainerName(opts.runId),
name: containerName,
image: opts.image,
ports: [
{
@@ -209,7 +213,7 @@ class KubernetesTaskOperations implements TaskOperations {
await this.#createPod(
{
metadata: {
name: `${this.#getRunContainerName(opts.runId)}-${randomUUID().slice(0, 8)}`,
name: `${this.#getRunContainerName(opts.runId)}-${opts.checkpointId.slice(-8)}`,
namespace: this.#namespace.metadata.name,
labels: {
...this.#getSharedLabels(opts),
@@ -313,6 +317,72 @@ class KubernetesTaskOperations implements TaskOperations {
await this.#getPod(opts.runId, this.#namespace);
}
async prePullDeployment(opts: TaskOperationsPrePullDeploymentOptions) {
const metaName = this.#getPrePullContainerName(opts.shortCode);
const metaLabels = {
...this.#getSharedLabels(opts),
app: "task-prepull",
"app.kubernetes.io/part-of": "trigger-worker",
"app.kubernetes.io/component": "prepull",
deployment: opts.deploymentId,
name: metaName,
} satisfies k8s.V1ObjectMeta["labels"];
await this.#createDaemonSet(
{
metadata: {
name: metaName,
namespace: this.#namespace.metadata.name,
labels: metaLabels,
},
spec: {
selector: {
matchLabels: {
name: metaName,
},
},
template: {
metadata: {
labels: metaLabels,
},
spec: {
...this.#defaultPodSpec,
restartPolicy: "Always",
initContainers: [
{
name: "prepull",
image: opts.imageRef,
command: ["/usr/bin/true"],
resources: {
limits: {
cpu: "0.25",
memory: "100Mi",
"ephemeral-storage": "1Gi",
},
},
},
],
containers: [
{
name: "pause",
image: "registry.k8s.io/pause:3.9",
resources: {
limits: {
cpu: "1m",
memory: "12Mi",
},
},
},
],
},
},
},
},
this.#namespace
);
}
#envTypeToLabelValue(type: EnvironmentType) {
switch (type) {
case "PRODUCTION":
@@ -402,7 +472,11 @@ class KubernetesTaskOperations implements TaskOperations {
}
#getSharedLabels(
opts: TaskOperationsIndexOptions | TaskOperationsCreateOptions | TaskOperationsRestoreOptions
opts:
| TaskOperationsIndexOptions
| TaskOperationsCreateOptions
| TaskOperationsRestoreOptions
| TaskOperationsPrePullDeploymentOptions
): Record<string, string> {
return {
env: opts.envId,
@@ -442,8 +516,12 @@ class KubernetesTaskOperations implements TaskOperations {
return `task-index-${suffix}`;
}
#getRunContainerName(suffix: string) {
return `task-run-${suffix}`;
#getRunContainerName(suffix: string, attemptNumber?: number) {
return `task-run-${suffix}${attemptNumber && attemptNumber > 1 ? `-att${attemptNumber}` : ""}`;
}
#getPrePullContainerName(suffix: string) {
return `task-prepull-${suffix}`;
}
#createK8sApi() {
@@ -460,6 +538,7 @@ class KubernetesTaskOperations implements TaskOperations {
return {
core: kubeConfig.makeApiClient(k8s.CoreV1Api),
batch: kubeConfig.makeApiClient(k8s.BatchV1Api),
apps: kubeConfig.makeApiClient(k8s.AppsV1Api),
};
}
@@ -503,6 +582,18 @@ class KubernetesTaskOperations implements TaskOperations {
}
}
async #createDaemonSet(daemonSet: k8s.V1DaemonSet, namespace: Namespace) {
try {
const res = await this.#k8sApi.apps.createNamespacedDaemonSet(
namespace.metadata.name,
daemonSet
);
logger.debug(res.body);
} catch (err: unknown) {
this.#handleK8sError(err);
}
}
#throwUnlessRecord(candidate: unknown): asserts candidate is Record<string, unknown> {
if (typeof candidate !== "object" || candidate === null) {
throw candidate;
@@ -15,6 +15,7 @@ export class PodCleaner {
private logger = new SimpleLogger("[PodCleaner]");
private k8sClient: {
core: k8s.CoreV1Api;
apps: k8s.AppsV1Api;
kubeConfig: k8s.KubeConfig;
};
@@ -43,6 +44,7 @@ export class PodCleaner {
return {
core: kubeConfig.makeApiClient(k8s.CoreV1Api),
apps: kubeConfig.makeApiClient(k8s.AppsV1Api),
kubeConfig: kubeConfig,
};
}
@@ -98,6 +100,25 @@ export class PodCleaner {
.catch(this.#handleK8sError.bind(this));
}
async #deleteDaemonSets(opts: {
namespace: string;
dryRun?: boolean;
fieldSelector?: string;
labelSelector?: string;
}) {
return await this.k8sClient.apps
.deleteCollectionNamespacedDaemonSet(
opts.namespace,
undefined, // pretty
undefined, // continue
opts.dryRun ? "All" : undefined,
opts.fieldSelector,
undefined, // gracePeriodSeconds
opts.labelSelector
)
.catch(this.#handleK8sError.bind(this));
}
async #deleteCompletedRuns() {
this.logger.log("Deleting completed runs");
@@ -152,6 +173,28 @@ export class PodCleaner {
});
}
async #deleteCompletedPrePulls() {
this.logger.log("Deleting completed pre-pulls");
const start = Date.now();
const result = await this.#deleteDaemonSets({
namespace: this.namespace,
labelSelector: "app=task-prepull",
});
const elapsedMs = Date.now() - start;
if (!result) {
this.logger.log("Deleting completed pre-pulls: No delete result", { elapsedMs });
return;
}
const total = (result.response as any)?.body?.items?.length ?? 0;
this.logger.log("Deleting completed pre-pulls: Done", { total, elapsedMs });
}
async start() {
this.enabled = true;
this.logger.log("Starting");
@@ -186,6 +229,22 @@ export class PodCleaner {
2 * this.intervalInSeconds * 1000
);
const completedPrePullInterval = setInterval(
async () => {
if (!this.enabled) {
clearInterval(completedPrePullInterval);
return;
}
try {
await this.#deleteCompletedPrePulls();
} catch (error) {
this.logger.error("Error deleting completed pre-pulls", error);
}
},
2 * this.intervalInSeconds * 1000
);
// this.#launchTests();
}
+33 -10
View File
@@ -1,13 +1,36 @@
{
"extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node", "prettier"],
"rules": {
"@typescript-eslint/strict-boolean-expressions": [
"error",
{
"allowNullableBoolean": true,
"allowNullableString": true,
"allowNullableNumber": true
"plugins": [
"@trigger.dev/eslint-plugin",
"react-hooks",
"@typescript-eslint/eslint-plugin",
"import"
],
"parser": "@typescript-eslint/parser",
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"rules": {
// Autofixes imports from "@trigger.dev/core" to fine grained modules
"@trigger.dev/no-trigger-core-import": "error",
// Normalize `import type {}` and `import { type }`
"@typescript-eslint/consistent-type-imports": [
"warn",
{
// the "type" annotation can get tangled and cause syntax errors
// during some autofixes, so easier to just turn it off
"prefer": "type-imports",
"disallowTypeAnnotations": true,
"fixStyle": "inline-type-imports"
}
],
// no-trigger-core-import splits imports into multiple lines
// this one merges them back into a single line
// if they still import from the same module
"import/no-duplicates": ["warn", { "prefer-inline": true }],
// lots of undeclared vars, enable this rule if you want to clean them up
"turbo/no-undeclared-env-vars": "off"
}
]
}
}
],
"ignorePatterns": ["seed.js", "seedCloud.ts", "populate.js"]
}
File diff suppressed because one or more lines are too long
@@ -15,11 +15,11 @@ export function DefinitionTip({
<TooltipProvider>
<Tooltip disableHoverableContent>
<TooltipTrigger>
<span className="underline decoration-charcoal-600 decoration-dashed underline-offset-4 transition hover:decoration-charcoal-500">
<span className="cursor-default underline decoration-charcoal-500 decoration-dashed underline-offset-4 transition hover:decoration-charcoal-400">
{children}
</span>
</TooltipTrigger>
<TooltipContent align="end" side="right" variant="dark" className="w-[16rem] min-w-[16rem]">
<TooltipContent align="end" side="right" className="w-[16rem] min-w-[16rem]">
<Header3 className="mb-1">{title}</Header3>
{typeof content === "string" ? (
<Paragraph variant="small">{content}</Paragraph>
+128 -108
View File
@@ -1,19 +1,25 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import {
CalendarDaysIcon,
ChevronRightIcon,
EnvelopeIcon,
LifebuoyIcon,
LightBulbIcon,
} from "@heroicons/react/24/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
import { DiscordIcon, GitHubLightIcon } from "@trigger.dev/companyicons";
import { DiscordIcon } from "@trigger.dev/companyicons";
import { ActivityIcon } from "lucide-react";
import { ReactNode, useState } from "react";
import { FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
import { type ReactNode, useState } from "react";
import { type FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
import { cn } from "~/utils/cn";
import { docsPath } from "~/utils/pathBuilder";
import { docsTroubleshootingPath } from "~/utils/pathBuilder";
import { Button, LinkButton } from "./primitives/Buttons";
import { Fieldset } from "./primitives/Fieldset";
import { FormButtons } from "./primitives/FormButtons";
import { FormError } from "./primitives/FormError";
import { Header1, Header2 } from "./primitives/Headers";
import { Header1 } from "./primitives/Headers";
import { InputGroup } from "./primitives/InputGroup";
import { Label } from "./primitives/Label";
import { Paragraph } from "./primitives/Paragraph";
@@ -55,123 +61,137 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
<SheetTrigger asChild={true}>{button}</SheetTrigger>
<SheetContent className="@container">
<SheetBody className="flex h-full flex-col justify-between">
<Header2 className="mb-2.5 text-xl">Get help from the community</Header2>
<Paragraph className="mb-4">
The quickest way to get help and feedback or to provide advice to others is to join our
Discord.
</Paragraph>
<div className="flex flex-col gap-x-4 @[30rem]:flex-row">
<DiscordBanner />
<GitHubDiscussionsBanner />
</div>
<hr className="mb-4" />
<Header2 className="mb-2.5 text-xl">Send us an email</Header2>
<Paragraph className="mb-4">We read every message and respond quickly.</Paragraph>
<Form method="post" action="/resources/feedback" {...form.props}>
<Fieldset className="max-w-full gap-y-3">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<InputGroup className="max-w-full">
<Select
{...conform.select(feedbackType)}
variant="tertiary/medium"
defaultValue={defaultValue}
placeholder="Select type"
text={(value) => feedbackTypeLabel[value]}
dropdownIcon
>
{Object.entries(feedbackTypeLabel).map(([name, title]) => (
<SelectItem key={name} value={name}>
{title}
</SelectItem>
))}
</Select>
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
</InputGroup>
<InputGroup className="max-w-full">
<Label>Message</Label>
<TextArea {...conform.textarea(message)} />
<FormError id={message.errorId}>{message.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<div className="flex w-full justify-end">
<FormButtons
className="m-0 w-max"
confirmButton={
<Button type="submit" variant="tertiary/medium">
Send message
</Button>
}
/>
</div>
</Fieldset>
</Form>
<hr className="my-4" />
<Header2 className="mb-2.5 text-xl">Troubleshooting</Header2>
<Paragraph className="mb-4">
If you're having trouble, check out our documentation or the Trigger.dev Status page.
</Paragraph>
<div className="flex flex-wrap gap-2">
<LinkButton to={docsPath("")} variant="tertiary/medium" LeadingIcon={BookOpenIcon}>
Docs
</LinkButton>
<LinkButton
to={"https://status.trigger.dev/"}
variant="tertiary/medium"
LeadingIcon={ActivityIcon}
>
Trigger.dev Status
</LinkButton>
</div>
<LinkBanner
title="Join our Discord community"
icon={<DiscordIcon className="size-9" />}
to="https://trigger.dev/discord"
className="hover:border-text-link"
>
<Paragraph>The quickest way to get answers from the Trigger.dev community.</Paragraph>
</LinkBanner>
<LinkBanner
title="Book a 15 min chat with the founders"
icon={<CalendarDaysIcon className="size-9 text-green-500" />}
to="https://cal.com/team/triggerdotdev/founders-call"
className="hover:border-green-500"
>
<Paragraph>Have a question or want to chat? Book a time to talk with us.</Paragraph>
</LinkBanner>
<LinkBanner
title="Suggest a feature"
icon={<LightBulbIcon className="size-9 text-sun-500" />}
to="https://feedback.trigger.dev/"
className="hover:border-sun-400"
>
<Paragraph>Have an idea for a new feature or improvement? Let us know!</Paragraph>
</LinkBanner>
<LinkBanner
title="Troubleshooting"
icon={<LifebuoyIcon className="size-9 text-rose-500" />}
>
<Paragraph>
If you're having trouble, check out our troubleshooting guide or the Trigger.dev
Status page.
</Paragraph>
<div className="flex flex-wrap gap-2">
<LinkButton
to={docsTroubleshootingPath("")}
variant="tertiary/medium"
LeadingIcon={BookOpenIcon}
>
Troubleshooting Docs
</LinkButton>
<LinkButton
to={"https://status.trigger.dev/"}
variant="tertiary/medium"
LeadingIcon={ActivityIcon}
>
Trigger.dev Status
</LinkButton>
</div>
</LinkBanner>
<LinkBanner
title="Send us an email"
icon={<EnvelopeIcon className="size-9 text-blue-500" />}
>
<Paragraph>We read every message and respond quickly.</Paragraph>
<Form method="post" action="/resources/feedback" {...form.props} className="w-full">
<Fieldset className="max-w-full gap-y-3">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<InputGroup className="max-w-full">
<Select
{...conform.select(feedbackType)}
variant="tertiary/medium"
defaultValue={defaultValue}
placeholder="Select type"
text={(value) => feedbackTypeLabel[value]}
dropdownIcon
>
{Object.entries(feedbackTypeLabel).map(([name, title]) => (
<SelectItem key={name} value={name}>
{title}
</SelectItem>
))}
</Select>
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
</InputGroup>
<InputGroup className="max-w-full">
<Label>Message</Label>
<TextArea {...conform.textarea(message)} />
<FormError id={message.errorId}>{message.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<div className="flex w-full justify-end">
<FormButtons
className="m-0 w-max"
confirmButton={
<Button type="submit" variant="tertiary/medium">
Send message
</Button>
}
/>
</div>
</Fieldset>
</Form>
</LinkBanner>
</SheetBody>
</SheetContent>
</Sheet>
);
}
function DiscordBanner({ className }: { className?: string }) {
function LinkBanner({
className,
icon,
title,
children,
to,
}: {
className?: string;
icon?: ReactNode;
title?: string;
children?: ReactNode;
to?: string;
}) {
return (
<a
href="https://trigger.dev/discord"
href={to}
target="_blank"
className={cn(
"group mb-4 flex w-full items-center justify-between rounded-md border border-charcoal-600 p-4 transition hover:border-text-link",
"group/banner mb-4 flex w-full items-center justify-between rounded-md border border-grid-bright bg-charcoal-750 p-4 transition",
className
)}
>
<div className="flex flex-col gap-y-2">
<DiscordIcon className="h-8 w-8" />
<Header1 className="text-2xl font-semibold text-text-bright transition group-hover:text-white">
Join our Discord community
</Header1>
<Paragraph variant="small" className="mb-4">
Get help or answer questions from the Trigger.dev community.
</Paragraph>
<div className="flex w-full items-start gap-4">
<span>{icon}</span>
<div className="flex w-full flex-col gap-2">
<Header1 className="text-2xl font-semibold text-text-bright">{title}</Header1>
{children}
</div>
</div>
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover:text-text-link" />
</a>
);
}
function GitHubDiscussionsBanner({ className }: { className?: string }) {
return (
<a
href="https://github.com/triggerdotdev/trigger.dev/discussions"
target="_blank"
className={cn(
"group mb-4 flex w-full items-center justify-between rounded-md border border-charcoal-600 p-4 transition hover:border-text-dimmed",
className
{to && (
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover/banner:text-text-bright" />
)}
>
<div className="flex flex-col gap-y-2">
<GitHubLightIcon className="mb-1 h-7 w-7" />
<Header1 className="text-2xl font-semibold text-text-bright transition group-hover:text-white">
View our GitHub Discussions
</Header1>
<Paragraph variant="small">
Post your questions, feedback, and feature requests on GitHub.
</Paragraph>
</div>
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover:text-text-bright" />
</a>
);
}
@@ -2,7 +2,6 @@ import { useEffect, useState } from "react";
import { ATAndTLogo } from "~/assets/logos/ATAndTLogo";
import { AppsmithLogo } from "~/assets/logos/AppsmithLogo";
import { CalComLogo } from "~/assets/logos/CalComLogo";
import { OktaLogo } from "~/assets/logos/OktaLogo";
import { ShopifyLogo } from "~/assets/logos/ShopifyLogo";
import { VerizonLogo } from "~/assets/logos/VerizonLogo";
import { LogoType } from "./LogoType";
@@ -75,7 +74,6 @@ export function LoginPageLayout({ children }: { children: React.ReactNode }) {
<div className="flex w-full flex-wrap items-center justify-center gap-x-6 gap-y-3 text-charcoal-500 xl:justify-between xl:gap-0">
<VerizonLogo />
<ShopifyLogo />
<OktaLogo />
<ATAndTLogo />
<AppsmithLogo />
<CalComLogo />
@@ -1,14 +1,16 @@
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
import * as Property from "~/components/primitives/PropertyTable";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/primitives/Tooltip";
import { useIsImpersonating } from "~/hooks/useOrganizations";
import { useHasAdminAccess } from "~/hooks/useUser";
import { useIsImpersonating, useOptionalOrganization } from "~/hooks/useOrganizations";
import { useOptionalProject } from "~/hooks/useProject";
import { useHasAdminAccess, useUser } from "~/hooks/useUser";
export function AdminDebugTooltip({ children }: { children: React.ReactNode }) {
export function AdminDebugTooltip({ children }: { children?: React.ReactNode }) {
const hasAdminAccess = useHasAdminAccess();
const isImpersonating = useIsImpersonating();
@@ -22,10 +24,46 @@ export function AdminDebugTooltip({ children }: { children: React.ReactNode }) {
<TooltipTrigger>
<ShieldCheckIcon className="size-5" />
</TooltipTrigger>
<TooltipContent className="flex max-h-[90vh] items-center gap-1 overflow-y-auto">
{children}
<TooltipContent className="max-h-[90vh] overflow-y-auto">
<Content>{children}</Content>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
function Content({ children }: { children: React.ReactNode }) {
const organization = useOptionalOrganization();
const project = useOptionalProject();
const user = useUser();
return (
<div className="flex flex-col gap-2 divide-y divide-slate-700">
<Property.Table>
<Property.Item>
<Property.Label>User ID</Property.Label>
<Property.Value>{user.id}</Property.Value>
</Property.Item>
{organization && (
<Property.Item>
<Property.Label>Org ID</Property.Label>
<Property.Value>{organization.id}</Property.Value>
</Property.Item>
)}
{project && (
<>
<Property.Item>
<Property.Label>Project ID</Property.Label>
<Property.Value>{project.id}</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Project ref</Property.Label>
<Property.Value>{project.ref}</Property.Value>
</Property.Item>
</>
)}
</Property.Table>
<div className="pt-2">{children}</div>
</div>
);
}
@@ -1,61 +0,0 @@
import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
import tileBgPath from "~/assets/images/error-banner-tile@2x.png";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { formatNumberCompact } from "~/utils/numberFormatter";
import { LinkButton } from "../primitives/Buttons";
import { Icon } from "../primitives/Icon";
import { Paragraph } from "../primitives/Paragraph";
type UpgradePromptProps = {
runsEnabled: boolean;
runCountCap: number;
planPath: string;
};
export function UpgradePrompt({ runsEnabled, runCountCap, planPath }: UpgradePromptProps) {
return (
<div
className="flex h-10 items-center justify-between border border-error bg-repeat py-0 pl-3 pr-2"
style={{ backgroundImage: `url(${tileBgPath})`, backgroundSize: "8px 8px" }}
>
<div className="flex items-center gap-2">
<Icon icon={ExclamationCircleIcon} className="h-5 w-5 text-error" />
<Paragraph variant="small" className="text-error">
{runsEnabled
? `You have exceeded the monthly ${formatNumberCompact(runCountCap)} runs
limit`
: `No runs are executing because you have exceeded the free limit`}
</Paragraph>
</div>
<LinkButton
variant={"primary/small"}
LeadingIcon={ArrowUpCircleIcon}
leadingIconClassName="px-0"
to={planPath}
>
Upgrade
</LinkButton>
</div>
);
}
export function useShowUpgradePrompt(organization?: MatchedOrganization) {
const currentPlan = useCurrentPlan();
const shouldShow =
organization !== undefined &&
currentPlan !== undefined &&
currentPlan.usage.exceededRunCount &&
currentPlan.usage.runCountCap !== undefined;
if (!shouldShow) {
return { shouldShow };
}
return {
shouldShow,
runCountCap: currentPlan.usage.runCountCap!,
runsEnabled: organization.runsEnabled,
};
}
@@ -8,7 +8,7 @@ import {
XAxis,
YAxis,
} from "recharts";
import { Paragraph } from "../primitives/Paragraph";
import { Paragraph } from "../../primitives/Paragraph";
const tooltipStyle = {
display: "flex",
@@ -1,5 +1,5 @@
import { Label, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { Paragraph } from "../primitives/Paragraph";
import { Paragraph } from "../../primitives/Paragraph";
const tooltipStyle = {
display: "flex",
@@ -1,6 +1,6 @@
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
import { motion, useMotionValue, useTransform } from "framer-motion";
import { Paragraph } from "../primitives/Paragraph";
import { Paragraph } from "../../primitives/Paragraph";
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
@@ -28,7 +28,7 @@ export function FreePlanUsage({ to, percentage }: { to: string; percentage: numb
<Paragraph className="text-2sm text-text-bright">Free Plan</Paragraph>
</div>
<Link to={to} className="text-2sm text-text-link">
Learn more
Upgrade
</Link>
</div>
<div className="relative mt-3 h-1 rounded-full bg-background-dimmed">
@@ -1,9 +1,9 @@
import * as Slider from "@radix-ui/react-slider";
import { Plans, estimate } from "@trigger.dev/billing";
import { Plans, estimate } from "@trigger.dev/platform/v2";
import { useCallback, useState } from "react";
import { DefinitionTip } from "../DefinitionTooltip";
import { Header2 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
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";
@@ -2,17 +2,17 @@ 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 { ActiveSubscription, Plan, Plans, SetPlanBodySchema } from "@trigger.dev/platform/v2";
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 { 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";
import { Spinner } from "../../primitives/Spinner";
const pricingDefinitions = {
concurrentRuns: {
@@ -1,6 +1,6 @@
import { RunPriceBracket } from "@trigger.dev/billing";
import { Header2 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
import { RunPriceBracket } from "@trigger.dev/platform/v2";
import { Header2 } from "../../primitives/Headers";
import { Paragraph } from "../../primitives/Paragraph";
import { formatNumberCompact } from "~/utils/numberFormatter";
export function RunsVolumeDiscountTable({
@@ -1,7 +1,7 @@
import { cn } from "~/utils/cn";
import { formatNumberCompact } from "~/utils/numberFormatter";
import { Paragraph } from "../primitives/Paragraph";
import { SimpleTooltip } from "../primitives/Tooltip";
import { Paragraph } from "../../primitives/Paragraph";
import { SimpleTooltip } from "../../primitives/Tooltip";
import { motion } from "framer-motion";
type UsageBarProps = {
@@ -0,0 +1,53 @@
import { ExclamationCircleIcon } from "@heroicons/react/20/solid";
import tileBgPath from "~/assets/images/error-banner-tile@2x.png";
import { MatchedOrganization, useOrganization } from "~/hooks/useOrganizations";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { v3BillingPath } from "~/utils/pathBuilder";
import { LinkButton } from "../../primitives/Buttons";
import { Icon } from "../../primitives/Icon";
import { Paragraph } from "../../primitives/Paragraph";
import { DateTime } from "~/components/primitives/DateTime";
export function UpgradePrompt() {
const organization = useOrganization();
const plan = useCurrentPlan();
if (!plan || !plan.v3Usage.hasExceededFreeTier) {
return null;
}
const nextMonth = new Date();
nextMonth.setUTCMonth(nextMonth.getMonth() + 1);
nextMonth.setUTCDate(1);
nextMonth.setUTCHours(0, 0, 0, 0);
return (
<div
className="flex h-10 items-center justify-between border border-error bg-repeat py-0 pl-3 pr-2"
style={{ backgroundImage: `url(${tileBgPath})`, backgroundSize: "8px 8px" }}
>
<div className="flex items-center gap-2">
<Icon icon={ExclamationCircleIcon} className="h-5 w-5 text-error" />
<Paragraph variant="small" className="text-error">
You have exceeded the monthly $
{(plan.v3Subscription?.plan?.limits.includedUsage ?? 500) / 100} free credits. No runs
will execute in Prod until{" "}
<DateTime date={nextMonth} includeTime={false} timeZone="utc" />, or you upgrade.
</Paragraph>
</div>
<LinkButton
variant={"primary/small"}
leadingIconClassName="px-0"
to={v3BillingPath(organization)}
>
Upgrade
</LinkButton>
</div>
);
}
export function useShowUpgradePrompt(organization?: MatchedOrganization) {
const currentPlan = useCurrentPlan();
const shouldShow = currentPlan?.v3Usage.hasExceededFreeTier === true;
return { shouldShow };
}
@@ -0,0 +1,170 @@
import { cn } from "~/utils/cn";
import { formatCurrency } from "~/utils/numberFormatter";
import { Paragraph } from "../../primitives/Paragraph";
import { SimpleTooltip } from "../../primitives/Tooltip";
import { motion } from "framer-motion";
type UsageBarProps = {
current: number;
billingLimit?: number;
tierLimit?: number;
projectedUsage?: number;
isPaying: boolean;
};
const startFactor = 4;
export function UsageBar({
current,
billingLimit,
tierLimit,
projectedUsage,
isPaying,
}: UsageBarProps) {
const getLargestNumber = Math.max(
current,
tierLimit ?? -Infinity,
projectedUsage ?? -Infinity,
billingLimit ?? -Infinity,
5
);
//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 = tierLimit ? Math.round((tierLimit / maxRange) * 100) : 0;
const projectedRunsPercentage = projectedUsage
? Math.round((projectedUsage / maxRange) * 100)
: 0;
const billingLimitPercentage =
billingLimit !== undefined ? Math.round((billingLimit / maxRange) * 100) : 0;
const usagePercentage = Math.round((current / maxRange) * 100);
//cap the usagePercentage to the freeRunLimitPercentage
const usageCappedToLimitPercentage = Math.min(usagePercentage, tierRunLimitPercentage);
return (
<div className="h-fit w-full py-12">
<div className="relative h-3 w-full rounded-sm bg-background-bright">
{billingLimit !== undefined && (
<motion.div
initial={{ width: billingLimitPercentage / startFactor + "%" }}
animate={{ width: billingLimitPercentage + "%" }}
transition={{ duration: 1.5, type: "spring" }}
style={{ width: `${billingLimitPercentage}%` }}
className="absolute h-3 rounded-l-sm"
>
<Legend
text="Billing limit:"
value={formatCurrency(billingLimit, false)}
position="bottomRow2"
percentage={billingLimitPercentage}
/>
</motion.div>
)}
{tierLimit !== undefined && (
<motion.div
initial={{ width: tierRunLimitPercentage / startFactor + "%" }}
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={isPaying ? `Included usage:` : `Tier limit:`}
value={formatCurrency(tierLimit, false)}
position="bottomRow1"
percentage={tierRunLimitPercentage}
/>
</motion.div>
)}
{projectedUsage !== undefined && projectedUsage !== 0 && (
<motion.div
initial={{ width: projectedRunsPercentage / startFactor + "%" }}
animate={{ width: projectedRunsPercentage + "%" }}
transition={{ duration: 1.5, type: "spring" }}
style={{ width: `${projectedRunsPercentage}%` }}
className="absolute h-3 rounded-l-sm"
>
<Legend
text="Projected:"
value={formatCurrency(projectedUsage, false)}
position="topRow2"
percentage={projectedRunsPercentage}
/>
</motion.div>
)}
<motion.div
initial={{ width: usagePercentage / startFactor + "%" }}
animate={{ width: usagePercentage + "%" }}
transition={{ duration: 1.5, type: "spring" }}
style={{ width: `${usagePercentage}%` }}
className={cn(
"absolute h-3 rounded-l-sm",
tierLimit && current > tierLimit ? "bg-rose-600" : "bg-green-600"
)}
>
<Legend
text="Used:"
value={formatCurrency(current, false)}
position="topRow1"
percentage={usagePercentage}
/>
</motion.div>
<motion.div
initial={{ width: usageCappedToLimitPercentage / startFactor + "%" }}
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-charcoal-500",
positions[position],
flipLegendPosition === true ? "-translate-x-full border-r" : "border-l"
)}
>
{tooltipContent ? (
<SimpleTooltip
button={
<Paragraph className="mr-px h-fit whitespace-nowrap bg-background-bright px-1.5 text-xs text-text-bright">
{text}
<span className="ml-1 text-text-dimmed">{value}</span>
</Paragraph>
}
side="top"
content={tooltipContent}
className="z-50 h-fit"
/>
) : (
<Paragraph className="mr-px h-fit whitespace-nowrap bg-background-bright px-1.5 text-xs text-text-bright">
{text}
<span className="ml-1 text-text-dimmed">{value}</span>
</Paragraph>
)}
</div>
);
}
+92 -83
View File
@@ -58,103 +58,116 @@ const extraLinesWhenClipping = 0.35;
const defaultTheme: PrismTheme = {
plain: {
color: "#9CDCFE",
color: "#9C9AF2",
backgroundColor: "rgba(0, 0, 0, 0)",
},
styles: [
{
types: ["prolog"],
types: ["comment", "prolog", "doctype", "cdata"],
style: {
color: "rgb(0, 0, 128)",
color: "#5F6570",
},
},
{
types: ["comment"],
style: {
color: "rgb(106, 153, 85)",
},
},
{
types: ["builtin", "changed", "keyword", "interpolation-punctuation"],
style: {
color: "rgb(86, 156, 214)",
},
},
{
types: ["number", "inserted"],
style: {
color: "rgb(181, 206, 168)",
},
},
{
types: ["constant"],
style: {
color: "rgb(100, 102, 149)",
},
},
{
types: ["attr-name", "variable"],
style: {
color: "rgb(156, 220, 254)",
},
},
{
types: ["deleted", "string", "attr-value", "template-punctuation"],
style: {
color: "rgb(206, 145, 120)",
},
},
{
types: ["selector"],
style: {
color: "rgb(215, 186, 125)",
},
},
{
// Fix tag color
types: ["tag"],
style: {
color: "rgb(78, 201, 176)",
},
},
{
// Fix tag color for HTML
types: ["tag"],
languages: ["markup"],
style: {
color: "rgb(86, 156, 214)",
},
},
{
types: ["punctuation", "operator"],
style: {
color: "rgb(212, 212, 212)",
},
},
{
// Fix punctuation color for HTML
types: ["punctuation"],
languages: ["markup"],
style: {
color: "#808080",
color: "#878C99",
},
},
{
types: ["function"],
types: ["property", "tag", "boolean", "number", "constant", "symbol", "deleted"],
style: {
color: "rgb(220, 220, 170)",
color: "#9B99FF",
},
},
{
types: ["class-name"],
types: ["selector", "attr-name", "string", "char", "builtin", "inserted"],
style: {
color: "rgb(78, 201, 176)",
color: "#AFEC73",
},
},
{
types: ["operator", "entity", "url"],
style: {
color: "#D4D4D4",
},
},
{
types: ["variable"],
style: {
color: "#CCCBFF",
},
},
{
types: ["atrule", "attr-value", "keyword"],
style: {
color: "#E888F8",
},
},
{
types: ["function", "class-name"],
style: {
color: "#D9F07C",
},
},
{
types: ["regex"],
style: {
color: "#d16969",
},
},
{
types: ["important", "bold"],
style: {
fontWeight: "bold",
},
},
{
types: ["italic"],
style: {
fontStyle: "italic",
},
},
{
types: ["namespace"],
style: {
opacity: 0.7,
},
},
{
types: ["deleted"],
style: {
color: "#F85149",
},
},
{
types: ["boolean"],
style: {
color: "#9B99FF",
},
},
{
types: ["char"],
style: {
color: "rgb(209, 105, 105)",
color: "#b5cea8",
},
},
{
types: ["tag"],
style: {
color: "#D7BA7D",
},
},
{
types: ["keyword.operator"],
style: {
color: "#8271ED",
},
},
{
types: ["meta.template.expression"],
style: {
color: "#d4d4d4",
},
},
],
@@ -228,16 +241,12 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
onMouseEnter={() => setMouseOver(true)}
onMouseLeave={() => setMouseOver(false)}
className={cn(
"absolute right-3 z-50 transition-colors duration-100 hover:cursor-pointer",
showChrome ? "top-10" : "top-3",
"absolute right-3 z-50 transition-colors duration-100 hover:cursor-pointer",
showChrome ? "top-10" : "top-2.5",
copied ? "text-emerald-500" : "text-charcoal-500 hover:text-charcoal-300"
)}
>
{copied ? (
<ClipboardCheck className="h-5 w-5" />
) : (
<Clipboard className="h-5 w-5" />
)}
{copied ? <ClipboardCheck className="size-4" /> : <Clipboard className="size-4" />}
</TooltipTrigger>
<TooltipContent side="left" className="text-xs">
{copied ? "Copied" : "Copy"}
@@ -378,8 +387,8 @@ function Chrome({ title }: { title?: string }) {
export function TitleRow({ title }: { title: string }) {
return (
<div className="flex items-center justify-between px-4">
<Paragraph variant="base/bright" className="w-full border-b border-grid-dimmed py-2.5">
<div className="flex items-center justify-between px-3">
<Paragraph variant="small/bright" className="w-full border-b border-grid-dimmed py-2">
{title}
</Paragraph>
</div>
+44 -32
View File
@@ -107,39 +107,51 @@ export function JSONEditor(opts: JSONEditorProps) {
}, 1500);
}, [view]);
const showButtons = showClearButton || showCopyButton;
return (
<div className={cn(opts.className, "grid grid-rows-[2.5rem_1fr]")}>
<div className="mx-3 flex items-center justify-end gap-2 border-b border-grid-dimmed">
{showClearButton && (
<Button
type="button"
variant="minimal/small"
TrailingIcon={TrashIcon}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
clear();
}}
>
Clear
</Button>
)}
{showCopyButton && (
<Button
type="button"
variant="minimal/small"
TrailingIcon={copied ? CheckIcon : ClipboardIcon}
trailingIconClassName={copied ? "text-green-500 group-hover:text-green-500" : undefined}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
copy();
}}
>
Copy
</Button>
)}
</div>
<div
className={cn(
opts.className,
"grid",
showButtons ? "grid-rows-[2.5rem_1fr]" : "grid-rows-[1fr]"
)}
>
{showButtons && (
<div className="mx-3 flex items-center justify-end gap-2 border-b border-grid-dimmed">
{showClearButton && (
<Button
type="button"
variant="minimal/small"
TrailingIcon={TrashIcon}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
clear();
}}
>
Clear
</Button>
)}
{showCopyButton && (
<Button
type="button"
variant="minimal/small"
TrailingIcon={copied ? CheckIcon : ClipboardIcon}
trailingIconClassName={
copied ? "text-green-500 group-hover:text-green-500" : undefined
}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
copy();
}}
>
Copy
</Button>
)}
</div>
)}
<div
className="w-full overflow-auto"
ref={editor}
@@ -1,6 +1,6 @@
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { cn } from "~/utils/cn";
import { useShowUpgradePrompt } from "../billing/UpgradePrompt";
import { useShowUpgradePrompt } from "../billing/v3/UpgradePrompt";
/** This container is used to surround the entire app, it correctly places the nav bar */
export function AppContainer({ children }: { children: React.ReactNode }) {
@@ -60,7 +60,9 @@ export function MainCenteredContainer({
}) {
return (
<div className="h-full w-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<div className={cn("mx-auto mt-[25vh] max-w-xs overflow-y-auto", className)}>{children}</div>
<div className={cn("mx-auto mt-6 max-w-xs overflow-y-auto p-1 md:mt-[22vh]", className)}>
{children}
</div>
</div>
);
}
+117 -103
View File
@@ -1,16 +1,18 @@
import {
AcademicCapIcon,
ArrowRightIcon,
ArrowRightOnRectangleIcon,
BeakerIcon,
BellAlertIcon,
ChartBarIcon,
ClockIcon,
CreditCardIcon,
CursorArrowRaysIcon,
IdentificationIcon,
KeyIcon,
RectangleStackIcon,
ServerStackIcon,
ShieldCheckIcon,
SignalIcon,
} from "@heroicons/react/20/solid";
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
import { useNavigation } from "@remix-run/react";
@@ -18,9 +20,9 @@ import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
import { Fragment, useEffect, useRef, useState } from "react";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { useFeatures } from "~/hooks/useFeatures";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { MatchedProject } from "~/hooks/useProject";
import { User } from "~/models/user.server";
import { type MatchedOrganization } from "~/hooks/useOrganizations";
import { type MatchedProject } from "~/hooks/useProject";
import { type User } from "~/models/user.server";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { cn } from "~/utils/cn";
import {
@@ -44,6 +46,8 @@ import {
projectSetupPath,
projectTriggersPath,
v3ApiKeysPath,
v3BillingPath,
v3ConcurrencyPath,
v3DeploymentsPath,
v3EnvironmentVariablesPath,
v3ProjectAlertsPath,
@@ -52,16 +56,16 @@ import {
v3RunsPath,
v3SchedulesPath,
v3TestPath,
v3UsagePath,
} 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 { FreePlanUsage } from "../billing/v2/FreePlanUsage";
import { Badge } from "../primitives/Badge";
import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { Button, LinkButton } from "../primitives/Buttons";
import { ClipboardField } from "../primitives/ClipboardField";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
import { Icon } from "../primitives/Icon";
@@ -75,8 +79,9 @@ import {
PopoverSectionHeader,
} from "../primitives/Popover";
import { StepNumber } from "../primitives/StepNumber";
import { TextLink } from "../primitives/TextLink";
import { SideMenuHeader } from "./SideMenuHeader";
import { MenuCount, SideMenuItem } from "./SideMenuItem";
import { SideMenuItem } from "./SideMenuItem";
type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
type SideMenuProject = Pick<
@@ -100,8 +105,11 @@ type SideMenuProps = {
export function SideMenu({ user, project, organization, organizations }: SideMenuProps) {
const borderRef = useRef<HTMLDivElement>(null);
const [showHeaderDivider, setShowHeaderDivider] = useState(false);
const { isManagedCloud } = useFeatures();
const currentPlan = useCurrentPlan();
const { isManagedCloud } = useFeatures();
const isV3Project = project.version === "V3";
const isFreeV3User = currentPlan?.v3Subscription?.isPaying === false;
useEffect(() => {
const handleScroll = () => {
@@ -173,16 +181,41 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
name="Team"
icon={UserGroupIcon}
to={organizationTeamPath(organization)}
iconColor="text-sky-500"
iconColor="text-amber-500"
data-action="team"
/>
<SideMenuItem
name={isManagedCloud ? "Usage & Billing" : "Usage"}
icon={ChartBarIcon}
to={organizationBillingPath(organization)}
iconColor="text-green-600"
data-action="usage & billing"
/>
{organization.projects.some((proj) => proj.version === "V3") && isManagedCloud && (
<>
<SideMenuItem
name="Usage"
icon={ChartBarIcon}
to={v3UsagePath(organization)}
iconColor="text-green-600"
data-action="usage"
/>
<SideMenuItem
name="Billing"
icon={CreditCardIcon}
to={v3BillingPath(organization)}
iconColor="text-blue-600"
data-action="billing"
badge={
currentPlan?.v3Subscription?.isPaying
? currentPlan?.v3Subscription?.plan?.title
: undefined
}
/>
</>
)}
{organization.projects.some((proj) => proj.version === "V2") && (
<SideMenuItem
name="Usage (v2)"
icon={ChartBarIcon}
to={organizationBillingPath(organization)}
iconColor="text-green-600"
data-action="usage & billing"
/>
)}
<SideMenuItem
name="Organization settings"
icon="settings"
@@ -193,22 +226,30 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
</div>
</div>
<div className="m-2">
{project.version === "V2" ? (
<Callout variant={"info"}>This is a v2 project</Callout>
) : (
<Callout variant={"idea"}>This is a v3 project in Developer Preview</Callout>
{project.version === "V2" && (
<div className="flex flex-col gap-3 rounded border border-success/50 bg-success/10 p-3">
<Paragraph variant="small/bright">
This is a v2 project. V2 will be deprecated on January 31, 2025.{" "}
<TextLink
className="text-text-bright underline decoration-text-dimmed underline-offset-2 transition hover:text-text-bright hover:decoration-text-bright"
to="https://trigger.dev/blog/v2-end-of-life-announcement"
>
Learn more
</TextLink>
.
</Paragraph>
<LinkButton
variant="primary/medium"
to="https://trigger.dev/docs/v3/upgrading-from-v2"
fullWidth
>
Upgrade to v3
</LinkButton>
</div>
)}
</div>
<div className="flex flex-col gap-1 border-t border-grid-bright p-1">
{project.version === "V2" && (
<SideMenuItem
to="https://trigger.dev/v3-early-access"
target="_blank"
name="Request access to v3"
icon={V3Icon}
/>
)}
{currentPlan?.subscription?.isPaying === true && (
{currentPlan?.v3Subscription?.plan?.limits.support === "slack" && (
<Dialog>
<DialogTrigger asChild>
<Button
@@ -263,23 +304,13 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
data-action="join our discord"
target="_blank"
/>
{project.version === "V2" ? (
<SideMenuItem
name="Documentation"
icon="docs"
to="https://trigger.dev/docs"
data-action="documentation"
target="_blank"
/>
) : (
<SideMenuItem
name="Documentation (v3)"
icon="docs"
to="https://trigger.dev/docs/v3"
data-action="documentation"
target="_blank"
/>
)}
<SideMenuItem
name="Documentation"
icon="docs"
to="https://trigger.dev/docs"
data-action="documentation"
target="_blank"
/>
<SideMenuItem
name="Changelog"
icon="star"
@@ -287,41 +318,31 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
data-action="changelog"
target="_blank"
/>
{project.version === "V2" ? (
<Feedback
button={
<Button
variant="small-menu-item"
LeadingIcon="log"
data-action="help & feedback"
fullWidth
textAlignLeft
>
Help & Feedback
</Button>
}
/>
) : (
<Feedback
defaultValue="developer preview"
button={
<Button
variant="small-menu-item"
LeadingIcon="log"
leadingIconClassName="text-primary"
data-action="help & feedback"
fullWidth
textAlignLeft
>
<span className="text-primary">Give feedback on v3</span>
</Button>
}
/>
)}
{currentPlan && !currentPlan.subscription?.isPaying && currentPlan.usage.runCountCap && (
<SideMenuItem
name="Status"
icon={SignalIcon}
to="https://status.trigger.dev/"
data-action="status"
target="_blank"
/>
<Feedback
button={
<Button
variant="small-menu-item"
LeadingIcon="log"
leadingIconClassName="text-primary"
data-action="help & feedback"
fullWidth
textAlignLeft
>
<span className="text-primary">Help & Feedback</span>
</Button>
}
/>
{isV3Project && isFreeV3User && (
<FreePlanUsage
to={organizationBillingPath(organization)}
percentage={currentPlan.usage.currentRunCount / currentPlan.usage.runCountCap}
to={v3BillingPath(organization)}
percentage={currentPlan.v3Usage.usagePercentage}
/>
)}
</div>
@@ -373,10 +394,10 @@ function ProjectSelector({
title={
<div className="flex w-full items-center justify-between text-text-bright">
<span className="grow truncate text-left">{p.name}</span>
{p.version === "V2" ? (
<MenuCount count={p.jobCount} />
) : (
<Badge variant="v3">v3</Badge>
{p.version === "V2" && (
<Badge variant="small" className="normal-case">
v2
</Badge>
)}
</div>
}
@@ -395,7 +416,7 @@ function ProjectSelector({
</div>
</Fragment>
))}
<div className="border-t border-charcoal-800 p-1">
<div className="border-t border-charcoal-700 p-1">
<PopoverMenuItem to={newOrganizationPath()} title="New Organization" icon="plus" />
</div>
</PopoverContent>
@@ -474,7 +495,7 @@ function V2ProjectSideMenu({
}) {
return (
<>
<SideMenuHeader title={"Project"}>
<SideMenuHeader title={"Project (v2)"}>
<PopoverMenuItem
to={projectSetupPath(organization, project)}
title="Framework setup"
@@ -485,7 +506,6 @@ function V2ProjectSideMenu({
name="Jobs"
icon="job"
iconColor="text-indigo-500"
count={project.jobCount}
to={projectPath(organization, project)}
data-action="jobs"
/>
@@ -513,7 +533,6 @@ function V2ProjectSideMenu({
name="HTTP endpoints"
icon="http-endpoint"
iconColor="text-pink-500"
count={project.httpEndpointCount}
to={projectHttpEndpointsPath(organization, project)}
data-action="httpendpoints"
/>
@@ -546,12 +565,11 @@ function V3ProjectSideMenu({
return (
<>
<SideMenuHeader title={"Project (v3)"} />
<SideMenuHeader title={"Project"} />
<SideMenuItem
name="Tasks"
icon={TaskIcon}
iconColor="text-blue-500"
count={project.jobCount}
to={v3ProjectPath(organization, project)}
data-action="tasks"
/>
@@ -589,6 +607,7 @@ function V3ProjectSideMenu({
to={v3EnvironmentVariablesPath(organization, project)}
data-action="environment variables"
/>
<SideMenuItem
name="Deployments"
icon={ServerStackIcon}
@@ -605,6 +624,13 @@ function V3ProjectSideMenu({
data-action="alerts"
/>
)}
<SideMenuItem
name="Concurrency limits"
icon={RectangleStackIcon}
iconColor="text-indigo-500"
to={v3ConcurrencyPath(organization, project)}
data-action="concurrency"
/>
<SideMenuItem
name="Project settings"
icon="settings"
@@ -615,15 +641,3 @@ function V3ProjectSideMenu({
</>
);
}
function V3Icon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="8" fill="#A8FF53" />
<path
d="M7.7488 6.17L5.5818 12H3.6678L1.5008 6.17H3.2058L4.6248 10.339L6.0328 6.17H7.7488ZM11.0527 7.292C11.6357 7.303 12.2297 7.006 12.2297 6.28C12.2297 5.774 11.7787 5.433 11.0527 5.433C10.4147 5.433 9.98567 5.741 9.91967 6.214L8.22567 6.126C8.36867 4.861 9.51267 4.014 11.0857 4.014C12.8457 4.014 13.9567 4.806 13.9567 6.049C13.9567 6.951 13.3847 7.534 12.3067 7.776C13.5387 8.04 14.2207 8.777 14.2207 9.855C14.2207 11.274 13.0107 12.176 11.0857 12.176C9.32567 12.176 8.12667 11.197 8.04967 9.712L9.75467 9.646C9.83167 10.405 10.4917 10.757 11.0967 10.757C11.8007 10.757 12.4937 10.394 12.4937 9.591C12.4937 8.81 11.7897 8.425 11.0527 8.447L10.3817 8.458V7.281L11.0527 7.292Z"
fill="#15171A"
/>
</svg>
);
}
@@ -1,8 +1,8 @@
import { AnchorHTMLAttributes } from "react";
import { type AnchorHTMLAttributes } from "react";
import { usePathName } from "~/hooks/usePathName";
import { cn } from "~/utils/cn";
import { LinkButton } from "../primitives/Buttons";
import { IconNames } from "../primitives/NamedIcon";
import { type IconNames } from "../primitives/NamedIcon";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
import { Icon } from "../primitives/Icon";
import { IconExclamationCircle } from "@tabler/icons-react";
@@ -13,7 +13,7 @@ export function SideMenuItem({
name,
to,
hasWarning,
count,
badge,
target,
subItem = false,
}: {
@@ -22,7 +22,7 @@ export function SideMenuItem({
name: string;
to: string;
hasWarning?: string | boolean;
count?: number;
badge?: string;
target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
subItem?: boolean;
}) {
@@ -47,20 +47,20 @@ export function SideMenuItem({
<div className="flex w-full items-center justify-between">
{name}
<div className="flex items-center gap-1">
{count !== undefined && count > 0 && <MenuCount count={count} />}
{badge !== undefined && <MenuCount count={badge} />}
{typeof hasWarning === "string" ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
<Icon icon={IconExclamationCircle} className="h-5 w-5 text-error" />
</TooltipTrigger>
<TooltipContent className="flex items-center gap-1 border border-rose-500 bg-rose-500/20 backdrop-blur-xl">
<TooltipContent className="flex items-center gap-1 border border-error bg-error/20 backdrop-blur-xl">
{hasWarning}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
hasWarning && <Icon icon={IconExclamationCircle} className="h-5 w-5 text-rose-500" />
hasWarning && <Icon icon={IconExclamationCircle} className="h-5 w-5 text-error" />
)}
</div>
</div>
@@ -68,8 +68,10 @@ export function SideMenuItem({
);
}
export function MenuCount({ count }: { count: number | string }) {
function MenuCount({ count }: { count: number | string }) {
return (
<div className="rounded-full bg-charcoal-900 px-2 py-1 text-xxs text-text-dimmed">{count}</div>
<div className="rounded-full bg-charcoal-900 px-2 py-1 text-xxs uppercase tracking-wider text-text-dimmed">
{count}
</div>
);
}
@@ -8,12 +8,8 @@ const Alert = AlertDialogPrimitive.Root;
const AlertTrigger = AlertDialogPrimitive.Trigger;
const AlertPortal = ({
className,
children,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
const AlertPortal = ({ children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal {...props}>
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
{children}
</div>
@@ -8,7 +8,6 @@ const variants = {
"grid place-items-center rounded-full px-[0.4rem] h-4 tracking-wider text-xxs bg-background-dimmed text-text-dimmed uppercase whitespace-nowrap",
outline:
"grid place-items-center rounded-sm px-1.5 h-5 tracking-wider text-xxs border border-dimmed text-text-dimmed uppercase whitespace-nowrap",
v3: "grid place-items-center rounded-full px-[0.4rem] h-5 tracking-wider text-xxs bg-charcoal-750 text-primary whitespace-nowrap",
"outline-rounded":
"grid place-items-center rounded-full px-1 h-4 tracking-wider text-xxs border border-blue-500 text-blue-500 uppercase whitespace-nowrap",
};
@@ -21,7 +21,7 @@ const sizes = {
shortcut: "-ml-0.5 -mr-1.5 rounded justify-self-center",
},
large: {
button: "h-10 px-2 text-sm font-medium",
button: "h-10 px-2 text-base font-medium",
icon: "h-5",
iconSpacing: "gap-x-0.5",
shortcutVariant: "medium" as const,
@@ -245,12 +245,12 @@ export function ButtonContent(props: ButtonContentPropsType) {
type ButtonPropsType = Pick<
JSX.IntrinsicElements["button"],
"type" | "disabled" | "onClick" | "name" | "value" | "form"
"type" | "disabled" | "onClick" | "name" | "value" | "form" | "autoFocus"
> &
React.ComponentProps<typeof ButtonContent>;
export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
({ type, disabled, onClick, ...props }, ref) => {
({ type, disabled, autoFocus, onClick, ...props }, ref) => {
const innerRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement);
@@ -276,6 +276,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
value={props.value}
ref={innerRef}
form={props.form}
autoFocus={autoFocus}
>
<ButtonContent {...props} />
</button>
@@ -0,0 +1,326 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "~/utils/cn";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-text-dimmed/50 [&_.recharts-cartesian-grid_line]:stroke-grid-bright/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-grid-dimmed [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-grid-dimmed [&_.recharts-radial-bar-background-sector]:fill-grid-dimmed [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-grid-dimmed [&_.recharts-reference-line-line]:stroke-grid-dimmed [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer className="!h-auto">
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES).map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`
),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-grid-bright/50 bg-background-dimmed px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-text-dimmed",
indicator === "dot" && "items-center"
)}
>
{formatter && item.value && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between gap-1.5 leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-text-dimmed">{itemConfig?.label || item.name}</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-text-bright">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-text-dimmed"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
});
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};
@@ -10,8 +10,8 @@ const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
const DialogPortal = ({ children, ...props }: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
@@ -2,7 +2,7 @@ import { cn } from "~/utils/cn";
const headerVariants = {
header1: {
text: "font-sans text-2xl leading-5 md:leading-6 lg:leading-7 font-semibold",
text: "font-sans text-2xl leading-5 md:leading-6 lg:leading-7 font-medium",
spacing: "mb-2",
},
header2: {
@@ -0,0 +1,71 @@
import { LinkButton } from "./Buttons";
import { Header3 } from "./Headers";
import { Paragraph } from "./Paragraph";
import { cn } from "~/utils/cn";
const variants = {
info: {
panelStyle: "border-grid-bright bg-background-bright",
},
upgrade: {
panelStyle: "border-indigo-400/20 bg-indigo-800/10",
},
};
type InfoPanelVariant = keyof typeof variants;
type Props = {
title?: string;
children: React.ReactNode;
to?: string;
buttonLabel?: string;
icon: React.ComponentType<any>;
iconClassName?: string;
variant?: InfoPanelVariant;
panelClassName?: string;
};
export function InfoPanel({
title,
children,
to,
buttonLabel,
icon,
iconClassName,
variant = "info",
panelClassName = "max-w-sm",
}: Props) {
const Icon = icon;
const variantStyle = variants[variant];
return (
<div
className={cn(
variantStyle.panelStyle,
title ? "flex-col" : "",
"flex h-fit items-start gap-3 rounded-md border p-4",
panelClassName
)}
>
<div className={cn("flex items-center gap-2", to ? "w-full justify-between" : "")}>
<Icon className={cn("size-5", iconClassName)} />
{to && (
<LinkButton to={to} variant="secondary/small">
{buttonLabel}
</LinkButton>
)}
</div>
<div className="flex flex-col gap-1">
{title && <Header3 className="text-text-bright">{title}</Header3>}
{typeof children === "string" ? (
<Paragraph variant={"small"} className="text-text-dimmed">
{children}
</Paragraph>
) : (
children
)}
</div>
</div>
);
}
@@ -1,10 +1,8 @@
import { ArrowUpRightIcon } from "@heroicons/react/20/solid";
import { Link, useNavigation } from "@remix-run/react";
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { useOptionalProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { plansPath } from "~/utils/pathBuilder";
import { UpgradePrompt, useShowUpgradePrompt } from "../billing/UpgradePrompt";
import { UpgradePrompt, useShowUpgradePrompt } from "../billing/v3/UpgradePrompt";
import { BreadcrumbIcon } from "./BreadcrumbIcon";
import { LinkButton } from "./Buttons";
import { Header2 } from "./Headers";
@@ -12,6 +10,7 @@ import { LoadingBarDivider } from "./LoadingBarDivider";
import { NamedIcon } from "./NamedIcon";
import { Paragraph } from "./Paragraph";
import { Tabs, TabsProps } from "./Tabs";
import { ReactNode } from "react";
type WithChildren = {
children: React.ReactNode;
@@ -23,27 +22,20 @@ export function NavBar({ children }: WithChildren) {
const showUpgradePrompt = useShowUpgradePrompt(organization);
const navigation = useNavigation();
const isLoading = navigation.state === "loading" || navigation.state === "submitting";
const project = useOptionalProject();
return (
<div>
<div className="grid h-10 w-full grid-rows-[auto_1px] bg-background-bright">
<div className="flex w-full items-center justify-between pl-3 pr-1">{children}</div>
<div className="flex w-full items-center justify-between pl-3 pr-2">{children}</div>
<LoadingBarDivider isLoading={isLoading} />
</div>
{showUpgradePrompt.shouldShow && organization && (
<UpgradePrompt
runsEnabled={showUpgradePrompt.runsEnabled}
runCountCap={showUpgradePrompt.runCountCap}
planPath={plansPath(organization)}
/>
)}
{showUpgradePrompt.shouldShow && organization && <UpgradePrompt />}
</div>
);
}
type PageTitleProps = {
title: string;
title: ReactNode;
backButton?: {
to: string;
text: string;
@@ -70,7 +62,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
}
export function PageAccessories({ children }: WithChildren) {
return <div className="flex items-center gap-3">{children}</div>;
return <div className="flex items-center gap-2">{children}</div>;
}
export function PageInfoRow({ children, className }: WithChildren) {
@@ -4,8 +4,8 @@ import { ChevronDownIcon, EllipsisVerticalIcon } from "@heroicons/react/24/solid
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "~/utils/cn";
import { ButtonContentPropsType, LinkButton } from "./Buttons";
import { Paragraph, ParagraphVariant } from "./Paragraph";
import { type ButtonContentPropsType, LinkButton } from "./Buttons";
import { Paragraph, type ParagraphVariant } from "./Paragraph";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
@@ -41,7 +41,7 @@ function PopoverSectionHeader({
variant?: ParagraphVariant;
}) {
return (
<Paragraph variant={variant} className="bg-charcoal-900 px-2.5 py-1.5">
<Paragraph variant={variant} className="bg-charcoal-750 px-2.5 py-1.5">
{title}
</Paragraph>
);
@@ -71,7 +71,10 @@ function PopoverMenuItem({
fullWidth
textAlignLeft
TrailingIcon={isSelected ? "check" : undefined}
className={isSelected ? "bg-charcoal-750 group-hover:bg-charcoal-800" : undefined}
className={cn(
"group-hover:bg-charcoal-700",
isSelected ? "bg-charcoal-750 group-hover:bg-charcoal-600/50" : undefined
)}
>
{title}
</LinkButton>
@@ -1,40 +1,31 @@
import { ReactNode } from "react";
import { type ReactNode } from "react";
import { Paragraph } from "./Paragraph";
import { cn } from "~/utils/cn";
export function PropertyTable({
children,
className,
}: {
type ChildrenClassName = {
children: ReactNode;
className?: string;
}) {
return (
<div className={cn("grid grid-cols-[auto,1fr] items-center gap-x-4 gap-y-2", className)}>
{children}
</div>
);
}
export type PropertyProps = {
label: ReactNode;
labelClassName?: string;
children: ReactNode;
};
export function Property({ label, labelClassName, children }: PropertyProps) {
return (
<>
<div className={labelClassName}>
{typeof label === "string" ? <Paragraph variant="small">{label}</Paragraph> : label}
</div>
<div>
{typeof children === "string" ? (
<Paragraph variant="small/bright">{children}</Paragraph>
) : (
children
)}
</div>
</>
);
function PropertyTable({ children, className }: { children: ReactNode; className?: string }) {
return <div className={cn("flex flex-col gap-y-3", className)}>{children}</div>;
}
function PropertyItem({ children, className }: ChildrenClassName) {
return <div className={cn("flex flex-col gap-0 text-sm", className)}>{children}</div>;
}
function PropertyLabel({ children, className }: ChildrenClassName) {
return <div className={cn("font-medium text-text-bright", className)}>{children}</div>;
}
function PropertyValue({ children, className }: ChildrenClassName) {
return <div className={cn("text-text-dimmed", className)}>{children}</div>;
}
export {
PropertyTable as Table,
PropertyItem as Item,
PropertyLabel as Label,
PropertyValue as Value,
};
@@ -28,8 +28,8 @@ interface SheetPortalProps
extends SheetPrimitive.DialogPortalProps,
VariantProps<typeof portalVariants> {}
const SheetPortal = ({ position, className, children, ...props }: SheetPortalProps) => (
<SheetPrimitive.Portal className={cn(className)} {...props}>
const SheetPortal = ({ position, children, ...props }: SheetPortalProps) => (
<SheetPrimitive.Portal {...props}>
<div className={portalVariants({ position })}>{children}</div>
</SheetPrimitive.Portal>
);
@@ -1,32 +1,37 @@
import { cn } from "~/utils/cn";
type CustomColor = {
background: string;
foreground: string;
};
export function Spinner({
className,
color = "blue",
}: {
className?: string;
color?: "blue" | "white" | "muted" | "dark";
color?: "blue" | "white" | "muted" | "dark" | CustomColor;
}) {
const colors = {
blue: {
light: "rgba(59, 130, 246, 0.4)",
dark: "rgba(59, 130, 246)",
background: "rgba(59, 130, 246, 0.4)",
foreground: "rgba(59, 130, 246)",
},
white: {
light: "rgba(255, 255, 255, 0.4)",
dark: "rgba(255, 255, 255)",
background: "rgba(255, 255, 255, 0.4)",
foreground: "rgba(255, 255, 255)",
},
muted: {
light: "#1C2433",
dark: "#3C4B62",
background: "#1C2433",
foreground: "#3C4B62",
},
dark: {
light: "#15171A",
dark: "#272A2E",
background: "#15171A",
foreground: "#272A2E",
},
};
const currentColor = colors[color];
const currentColor = typeof color === "string" ? colors[color] : color;
return (
<svg
@@ -37,13 +42,33 @@ export function Spinner({
xmlns="http://www.w3.org/2000/svg"
className={cn("animate-spin motion-reduce:hidden", className)}
>
<rect x="2" y="2" width="16" height="16" rx="8" stroke={currentColor.light} strokeWidth="3" />
<rect
x="2"
y="2"
width="16"
height="16"
rx="8"
stroke={currentColor.background}
strokeWidth="3"
/>
<path
d="M10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2"
stroke={currentColor.dark}
stroke={currentColor.foreground}
strokeWidth="3"
strokeLinecap="round"
/>
</svg>
);
}
export function ButtonSpinner() {
return (
<Spinner
className="size-3"
color={{
foreground: "rgba(0, 0, 0, 1)",
background: "rgba(0, 0, 0, 0.25)",
}}
/>
);
}
@@ -3,6 +3,7 @@ import { Link } from "@remix-run/react";
import { ReactNode, forwardRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Popover, PopoverContent, PopoverVerticalEllipseTrigger } from "./Popover";
import { InfoIconTooltip } from "./Tooltip";
type TableProps = {
containerClassName?: string;
@@ -87,10 +88,11 @@ type TableCellBasicProps = {
type TableHeaderCellProps = TableCellBasicProps & {
hiddenLabel?: boolean;
tooltip?: ReactNode;
};
export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellProps>(
({ className, alignment = "left", children, colSpan, hiddenLabel = false }, ref) => {
({ className, alignment = "left", children, colSpan, hiddenLabel = false, tooltip }, ref) => {
let alignmentClassName = "text-left";
switch (alignment) {
case "center":
@@ -112,7 +114,16 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
)}
colSpan={colSpan}
>
{hiddenLabel ? <span className="sr-only">{children}</span> : children}
{hiddenLabel ? (
<span className="sr-only">{children}</span>
) : tooltip ? (
<div className="flex items-center gap-1">
{children}
<InfoIconTooltip content={tooltip} contentClassName="normal-case tracking-normal" />
</div>
) : (
children
)}
</th>
);
}
+104 -21
View File
@@ -1,6 +1,11 @@
import { NavLink, useLocation } from "@remix-run/react";
import { Link, NavLink, useLocation } from "@remix-run/react";
import { motion } from "framer-motion";
import { ReactNode, useRef } from "react";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { projectPubSub } from "~/v3/services/projectPubSub.server";
import { ShortcutKey } from "./ShortcutKey";
export type TabsProps = {
tabs: {
@@ -13,28 +18,106 @@ export type TabsProps = {
export function Tabs({ tabs, className, layoutId }: TabsProps) {
return (
<div className={cn(`flex flex-row gap-x-6 border-b border-grid-bright`, className)}>
<TabContainer className={className}>
{tabs.map((tab, index) => (
<NavLink key={index} to={tab.to} className="group flex flex-col items-center pt-1" end>
{({ isActive, isPending }) => (
<>
<span
className={cn(
"text-sm transition duration-200",
isActive || isPending ? "text-indigo-500" : "text-charcoal-200"
)}
>
{tab.label}
</span>
{isActive || isPending ? (
<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-charcoal-500 opacity-0 transition duration-200 group-hover:opacity-100" />
)}
</>
)}
</NavLink>
<TabLink key={index} to={tab.to} layoutId={layoutId}>
{tab.label}
</TabLink>
))}
</TabContainer>
);
}
export function TabContainer({ children, className }: { children: ReactNode; className?: string }) {
return (
<div className={cn(`flex flex-row gap-x-6 border-b border-grid-bright`, className)}>
{children}
</div>
);
}
export function TabLink({
to,
children,
layoutId,
}: {
to: string;
children: ReactNode;
layoutId: string;
}) {
return (
<NavLink to={to} className="group flex flex-col items-center pt-1" end>
{({ isActive, isPending }) => {
return (
<>
<span
className={cn(
"text-sm transition duration-200",
isActive || isPending ? "text-text-bright" : "text-text-bright"
)}
>
{children}
</span>
{isActive || isPending ? (
<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-charcoal-500 opacity-0 transition duration-200 group-hover:opacity-100" />
)}
</>
);
}}
</NavLink>
);
}
export function TabButton({
isActive,
layoutId,
shortcut,
...props
}: {
isActive: boolean;
shortcut?: ShortcutDefinition;
layoutId: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
const ref = useRef<HTMLButtonElement>(null);
if (shortcut) {
useShortcutKeys({
shortcut: shortcut,
action: () => {
if (ref.current) {
ref.current.click();
}
},
disabled: props.disabled,
});
}
return (
<button
className={cn("group flex flex-col items-center pt-1", props.className)}
ref={ref}
{...props}
>
<>
<div className="flex items-center gap-1">
<span
className={cn(
"text-sm transition duration-200",
isActive ? "text-text-bright" : "text-text-bright"
)}
>
{props.children}
</span>
{shortcut && <ShortcutKey className={cn("")} shortcut={shortcut} variant={"small"} />}
</div>
{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-charcoal-500 opacity-0 transition duration-200 group-hover:opacity-100" />
)}
</>
</button>
);
}
@@ -5,7 +5,7 @@ import { InformationCircleIcon } from "@heroicons/react/20/solid";
const variantClasses = {
basic:
"bg-background-dimmed border border-charcoal-700 rounded-md px-3 py-1.5 text-sm text-text-bright shadow-md fade-in-50",
"bg-background-bright border border-grid-bright rounded px-3 py-2 text-sm text-text-bright shadow-md fade-in-50",
dark: "bg-background-dimmed border border-grid-bright rounded px-3 py-2 text-sm text-text-bright shadow-md fade-in-50",
};
@@ -58,6 +58,7 @@ function SimpleTooltip({
variant,
disableHoverableContent = false,
className,
buttonClassName,
}: {
button: React.ReactNode;
content: React.ReactNode;
@@ -66,11 +67,12 @@ function SimpleTooltip({
variant?: Variant;
disableHoverableContent?: boolean;
className?: string;
buttonClassName?: string;
}) {
return (
<TooltipProvider disableHoverableContent={disableHoverableContent}>
<Tooltip>
<TooltipTrigger className="h-fit">{button}</TooltipTrigger>
<TooltipTrigger className={cn("h-fit", buttonClassName)}>{button}</TooltipTrigger>
<TooltipContent
side={side}
hidden={hidden}
@@ -87,17 +89,22 @@ function SimpleTooltip({
export function InfoIconTooltip({
content,
buttonClassName,
contentClassName,
variant = "basic",
}: {
content: React.ReactNode;
buttonClassName?: string;
contentClassName?: string;
variant?: Variant;
}) {
return (
<SimpleTooltip
button={
<InformationCircleIcon className={cn("h-3.5 w-3.5 text-text-dimmed", buttonClassName)} />
<InformationCircleIcon className={cn("size-3.5 text-text-dimmed", buttonClassName)} />
}
content={content}
variant="dark"
variant={variant}
className={contentClassName}
/>
);
}
@@ -23,8 +23,7 @@ export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialog
<DialogContent key="cancel">
<DialogHeader>Cancel this run?</DialogHeader>
<DialogDescription>
Canceling a run will stop execution. If you want to run this later you will have to replay
the entire run with the original payload.
Canceling a run will stop execution, along with any executing subtasks.
</DialogDescription>
<DialogFooter>
<Form action={`/resources/taskruns/${runFriendlyId}/cancel`} method="post">
@@ -11,22 +11,32 @@ import { cn } from "~/utils/cn";
export function DeploymentStatus({
status,
isBuilt,
className,
}: {
status: WorkerDeploymentStatus;
isBuilt: boolean;
className?: string;
}) {
return (
<span className={cn("flex items-center gap-1", className)}>
<DeploymentStatusIcon status={status} className="h-4 w-4" />
<DeploymentStatusLabel status={status} />
<DeploymentStatusLabel status={status} isBuilt={isBuilt} />
</span>
);
}
export function DeploymentStatusLabel({ status }: { status: WorkerDeploymentStatus }) {
export function DeploymentStatusLabel({
status,
isBuilt,
}: {
status: WorkerDeploymentStatus;
isBuilt: boolean;
}) {
return (
<span className={deploymentStatusClassNameColor(status)}>{deploymentStatusTitle(status)}</span>
<span className={deploymentStatusClassNameColor(status)}>
{deploymentStatusTitle(status, isBuilt)}
</span>
);
}
@@ -79,7 +89,7 @@ export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus):
}
}
export function deploymentStatusTitle(status: WorkerDeploymentStatus): string {
export function deploymentStatusTitle(status: WorkerDeploymentStatus, isBuilt: boolean): string {
switch (status) {
case "PENDING":
return "Pending…";
@@ -92,9 +102,17 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus): string {
case "CANCELED":
return "Canceled";
case "TIMED_OUT":
return "Timed out";
if (!isBuilt) {
return "Build timed out";
}
return "Indexing timed out";
case "FAILED":
return "Failed";
if (!isBuilt) {
return "Build failed";
}
return "Indexing failed";
default: {
assertNever(status);
}
@@ -23,11 +23,11 @@ export function LiveTimer({
}, updateInterval);
return () => clearInterval(interval);
}, [startTime]);
}, [startTime, endTime]);
return (
<>
{formatDuration(startTime, now, {
{formatDuration(startTime, endTime ?? now, {
style: "short",
maxDecimalPoints: 0,
units: ["d", "h", "m", "s"],
@@ -1,12 +1,16 @@
import { ArrowPathIcon } from "@heroicons/react/20/solid";
import { Form, useFetcher, useNavigation } from "@remix-run/react";
import { Form, useFetcher, useNavigation, useSubmit } from "@remix-run/react";
import { useCallback, useEffect, useRef } from "react";
import { UseDataFunctionReturn, useTypedFetcher } from "remix-typedjson";
import { JSONEditor } from "~/components/code/JSONEditor";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { Button } from "~/components/primitives/Buttons";
import {
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
} from "~/components/primitives/Dialog";
import { DialogContent, DialogDescription, DialogHeader } from "~/components/primitives/Dialog";
import { Header3 } from "~/components/primitives/Headers";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Select, SelectItem } from "~/components/primitives/Select";
import { ButtonSpinner, Spinner } from "~/components/primitives/Spinner";
import { type loader } from "~/routes/resources.taskruns.$runParam.replay";
type ReplayRunDialogProps = {
runFriendlyId: string;
@@ -14,31 +18,142 @@ type ReplayRunDialogProps = {
};
export function ReplayRunDialog({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) {
const navigation = useNavigation();
const formAction = `/resources/taskruns/${runFriendlyId}/replay`;
const isLoading = navigation.formAction === formAction;
return (
<DialogContent key="replay">
<DialogHeader>Replay this run?</DialogHeader>
<DialogDescription>
Replaying a run will create a new run with the same payload and environment as the original.
</DialogDescription>
<DialogFooter>
<Form action={formAction} method="post">
<input type="hidden" name="failedRedirect" value={failedRedirect} />
<Button
type="submit"
variant="primary/small"
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
disabled={isLoading}
shortcut={{ modifiers: ["meta"], key: "enter" }}
>
{isLoading ? "Replaying..." : "Replay run"}
</Button>
</Form>
</DialogFooter>
<DialogContent key={`replay`} className="md:max-w-3xl">
<ReplayContent runFriendlyId={runFriendlyId} failedRedirect={failedRedirect} />
</DialogContent>
);
}
function ReplayContent({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) {
const fetcher = useTypedFetcher<typeof loader>();
const isLoading = fetcher.state !== "idle";
useEffect(() => {
fetcher.load(`/resources/taskruns/${runFriendlyId}/replay`);
}, [runFriendlyId]);
return (
<>
<DialogHeader>Replay this run</DialogHeader>
{isLoading ? (
<div className="grid place-items-center p-6">
<Spinner />
</div>
) : fetcher.data ? (
<ReplayForm
{...fetcher.data}
failedRedirect={failedRedirect}
runFriendlyId={runFriendlyId}
/>
) : (
<>Failed to get run data</>
)}
</>
);
}
function ReplayForm({
payload,
payloadType,
environment,
environments,
failedRedirect,
runFriendlyId,
}: UseDataFunctionReturn<typeof loader> & { failedRedirect: string; runFriendlyId: string }) {
const navigation = useNavigation();
const submit = useSubmit();
const currentJson = useRef<string>(payload);
const formAction = `/resources/taskruns/${runFriendlyId}/replay`;
const isSubmitting = navigation.formAction === formAction;
const editablePayload =
payloadType === "application/json" || payloadType === "application/super+json";
const submitForm = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
const formData = new FormData(e.currentTarget);
const data: Record<string, string> = {
environment: formData.get("environment") as string,
failedRedirect: formData.get("failedRedirect") as string,
};
if (editablePayload) {
data.payload = currentJson.current;
}
submit(data, {
action: formAction,
method: "post",
});
e.preventDefault();
},
[currentJson]
);
return (
<Form action={formAction} method="post" onSubmit={(e) => submitForm(e)} className="pt-2">
{editablePayload ? (
<>
<Header3 spacing>Payload</Header3>
<div className="mb-3 max-h-[70vh] overflow-y-auto rounded-sm border border-grid-dimmed bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<JSONEditor
autoFocus
defaultValue={currentJson.current}
readOnly={false}
basicSetup
onChange={(v) => {
currentJson.current = v;
}}
showClearButton={false}
showCopyButton={false}
height="100%"
min-height="100%"
max-height="100%"
/>
</div>
</>
) : null}
<InputGroup>
<Label>Environment</Label>
<Select
id="environment"
name="environment"
placeholder="Select an environment"
defaultValue={environment.id}
items={environments}
dropdownIcon
variant="tertiary/medium"
className="w-fit pl-2"
text={(value) => {
const env = environments.find((env) => env.id === value)!;
return (
<div className="flex items-center pr-2">
<EnvironmentLabel environment={env} userName={env.userName} />
</div>
);
}}
>
{(matches) =>
matches.map((env) => (
<SelectItem key={env.id} value={env.id}>
<EnvironmentLabel environment={env} userName={env.userName} />
</SelectItem>
))
}
</Select>
</InputGroup>
<input type="hidden" name="failedRedirect" value={failedRedirect} />
<Button
type="submit"
variant="primary/medium"
LeadingIcon={isSubmitting ? ButtonSpinner : undefined}
disabled={isSubmitting}
shortcut={{ modifiers: ["meta"], key: "enter", enabledOnInputElements: true }}
className="mt-5"
>
{isSubmitting ? "Replaying..." : "Replay run"}
</Button>
</Form>
);
}
@@ -0,0 +1,55 @@
import { ArrowPathIcon } from "@heroicons/react/20/solid";
import { Form, useNavigation } from "@remix-run/react";
import { Button } from "~/components/primitives/Buttons";
import {
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
} from "~/components/primitives/Dialog";
type RetryDeploymentIndexingDialogProps = {
projectId: string;
deploymentShortCode: string;
redirectPath: string;
};
export function RetryDeploymentIndexingDialog({
projectId,
deploymentShortCode,
redirectPath,
}: RetryDeploymentIndexingDialogProps) {
const navigation = useNavigation();
const formAction = `/resources/${projectId}/deployments/${deploymentShortCode}/retry-indexing`;
const isLoading = navigation.formAction === formAction;
return (
<DialogContent key="retry-indexing">
<DialogHeader>Retry indexing this deployment?</DialogHeader>
<DialogDescription>
Retrying can be useful if indexing failed due to missing environment variables. Make sure
you set them before retrying. In most other cases, indexing will keep failing until you fix
any errors and re-deploy.
</DialogDescription>
<DialogFooter>
<Form
action={`/resources/${projectId}/deployments/${deploymentShortCode}/retry-indexing`}
method="post"
>
<Button
type="submit"
name="redirectUrl"
value={redirectPath}
variant="primary/small"
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
disabled={isLoading}
shortcut={{ modifiers: ["meta"], key: "enter" }}
>
{isLoading ? "Retrying..." : "Retry indexing"}
</Button>
</Form>
</DialogFooter>
</DialogContent>
);
}
@@ -4,9 +4,10 @@ import {
CalendarIcon,
CpuChipIcon,
InboxStackIcon,
TagIcon,
XMarkIcon,
} from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import { Form, useFetcher } from "@remix-run/react";
import type {
RuntimeEnvironment,
TaskTriggerSource,
@@ -15,7 +16,7 @@ import type {
} from "@trigger.dev/database";
import { ListFilterIcon } from "lucide-react";
import type { ReactNode } from "react";
import { startTransition, useCallback, useMemo, useState } from "react";
import { startTransition, useCallback, useEffect, useMemo, useState } from "react";
import { z } from "zod";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
@@ -51,6 +52,10 @@ import {
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
import { DateTime } from "~/components/primitives/DateTime";
import { BulkActionStatusCombo } from "./BulkAction";
import { type loader } from "~/routes/resources.projects.$projectParam.runs.tags";
import { useProject } from "~/hooks/useProject";
import { Spinner } from "~/components/primitives/Spinner";
import { matchSorter } from "match-sorter";
export const TaskAttemptStatus = z.enum(allTaskRunStatuses);
@@ -73,6 +78,10 @@ export const TaskRunListSearchFilters = z.object({
(value) => (typeof value === "string" ? [value] : value),
TaskAttemptStatus.array().optional()
),
tags: z.preprocess(
(value) => (typeof value === "string" ? [value] : value),
z.string().array().optional()
),
period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()),
bulkId: z.string().optional(),
from: z.coerce.number().optional(),
@@ -104,7 +113,8 @@ export function RunsFilters(props: RunFiltersProps) {
searchParams.has("environments") ||
searchParams.has("tasks") ||
searchParams.has("period") ||
searchParams.has("bulkId");
searchParams.has("bulkId") ||
searchParams.has("tags");
return (
<div className="flex flex-row flex-wrap items-center gap-1">
@@ -133,6 +143,7 @@ const filterTypes = [
},
{ name: "environments", title: "Environment", icon: <CpuChipIcon className="size-4" /> },
{ name: "tasks", title: "Tasks", icon: <TaskIcon className="size-4" /> },
{ name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> },
{ name: "created", title: "Created", icon: <CalendarIcon className="size-4" /> },
{ name: "bulk", title: "Bulk action", icon: <InboxStackIcon className="size-4" /> },
] as const;
@@ -209,6 +220,7 @@ function AppliedFilters({ possibleEnvironments, possibleTasks, bulkActions }: Ru
<AppliedStatusFilter />
<AppliedEnvironmentFilter possibleEnvironments={possibleEnvironments} />
<AppliedTaskFilter possibleTasks={possibleTasks} />
<AppliedTagsFilter />
<AppliedPeriodFilter />
<AppliedBulkActionsFilter bulkActions={bulkActions} />
</>
@@ -237,6 +249,8 @@ function Menu(props: MenuProps) {
return <CreatedDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "bulk":
return <BulkActionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "tags":
return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
}
}
@@ -648,6 +662,124 @@ function AppliedBulkActionsFilter({ bulkActions }: Pick<RunFiltersProps, "bulkAc
);
}
function TagsDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
}) {
const project = useProject();
const { values, replace } = useSearchParams();
const handleChange = (values: string[]) => {
clearSearchValue();
replace({
tags: values,
cursor: undefined,
direction: undefined,
});
};
const fetcher = useFetcher<typeof loader>();
useEffect(() => {
const searchParams = new URLSearchParams();
if (searchValue) {
searchParams.set("name", encodeURIComponent(searchValue));
}
fetcher.load(`/resources/projects/${project.slug}/runs/tags?${searchParams}`);
}, [searchValue]);
const filtered = useMemo(() => {
let items: string[] = [];
if (searchValue === "") {
items = values("tags");
}
if (fetcher.data === undefined) {
return matchSorter(items, searchValue);
}
items.push(...fetcher.data.tags.map((t) => t.name));
return matchSorter(Array.from(new Set(items)), searchValue);
}, [searchValue, fetcher.data]);
return (
<SelectProvider value={values("tags")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox
value={searchValue}
render={(props) => (
<div className="flex items-center justify-stretch">
<input {...props} placeholder={"Filter by tags..."} />
{fetcher.state === "loading" && <Spinner color="muted" />}
</div>
)}
/>
<SelectList>
{filtered.length > 0
? filtered.map((tag, index) => (
<SelectItem key={tag} value={tag}>
{tag}
</SelectItem>
))
: null}
{filtered.length === 0 && fetcher.state !== "loading" && (
<SelectItem disabled>No tags found</SelectItem>
)}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
function AppliedTagsFilter() {
const { values, del } = useSearchParams();
const tags = values("tags");
if (tags.length === 0) {
return null;
}
return (
<FilterMenuProvider>
{(search, setSearch) => (
<TagsDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer" />}>
<AppliedFilter
label="Tags"
value={appliedSummary(values("tags"))}
onRemove={() => del(["tags", "cursor", "direction"])}
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
const timePeriods = [
{
label: "All periods",
@@ -3,6 +3,7 @@ import {
HandRaisedIcon,
InformationCircleIcon,
Squares2X2Icon,
TagIcon,
} from "@heroicons/react/20/solid";
import { AttemptIcon } from "~/assets/icons/AttemptIcon";
import { TaskIcon } from "~/assets/icons/TaskIcon";
@@ -48,6 +49,8 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
return <ClockIcon className={cn(className, "text-teal-500")} />;
case "trace":
return <Squares2X2Icon className={cn(className, "text-text-dimmed")} />;
case "tag":
return <TagIcon className={cn(className, "text-text-dimmed")} />;
//log levels
case "debug":
case "log":
@@ -0,0 +1,48 @@
import { useMemo } from "react";
import tagLeftPath from "./tag-left.svg";
type Tag = string | { key: string; value: string };
export function RunTag({ tag }: { tag: string }) {
const tagResult = useMemo(() => splitTag(tag), [tag]);
if (typeof tagResult === "string") {
return (
<span className="flex h-6 items-stretch">
<img src={tagLeftPath} alt="" className="block h-full w-[0.5625rem]" />
<span className="flex items-center rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed">
{tag}
</span>
</span>
);
} else {
return (
<span className="flex h-6 items-stretch">
<img src={tagLeftPath} alt="" className="block h-full w-[0.5625rem]" />
<span className="flex items-center border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed">
{tagResult.key}
</span>
<span className="flex items-center rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-750 px-1.5 text-text-dimmed">
{tagResult.value}
</span>
</span>
);
}
}
/** Takes a string and turns it into a tag
*
* If the string has 12 or fewer alpha characters followed by an underscore or colon then we return an object with a key and value
* Otherwise we return the original string
*/
function splitTag(tag: string): Tag {
if (tag.match(/^[a-zA-Z]{1,12}[_:]/)) {
const components = tag.split(/[_:]/);
if (components.length !== 2) {
return tag;
}
return { key: components[0], value: components[1] };
}
return tag;
}
@@ -1,6 +1,6 @@
import { XMarkIcon } from "@heroicons/react/20/solid";
import { useNavigate } from "@remix-run/react";
import { RuntimeEnvironment } from "@trigger.dev/database";
import { type RuntimeEnvironment } from "@trigger.dev/database";
import { useCallback } from "react";
import { z } from "zod";
import { Input } from "~/components/primitives/Input";
@@ -17,6 +17,7 @@ import {
SelectTrigger,
SelectValue,
} from "../../primitives/SimpleSelect";
import { ScheduleTypeCombo } from "./ScheduleType";
export const ScheduleListFilters = z.object({
page: z.coerce.number().default(1),
@@ -28,6 +29,7 @@ export const ScheduleListFilters = z.object({
.string()
.optional()
.transform((value) => (value ? value.split(",") : undefined)),
type: z.union([z.literal("declarative"), z.literal("imperative")]).optional(),
search: z.string().optional(),
});
@@ -48,7 +50,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
const navigate = useNavigate();
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const { environments, tasks, page, search } = ScheduleListFilters.parse(
const { environments, tasks, page, search, type } = ScheduleListFilters.parse(
Object.fromEntries(searchParams.entries())
);
@@ -73,6 +75,10 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
handleFilterChange("environments", value === "ALL" ? undefined : value);
}, []);
const handleTypeChange = useCallback((value: string | typeof All) => {
handleFilterChange("type", value === "ALL" ? undefined : value);
}, []);
const handleSearchChange = useThrottle((value: string) => {
handleFilterChange("search", value.length === 0 ? undefined : value);
}, 300);
@@ -97,6 +103,30 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
defaultValue={search}
onChange={(e) => handleSearchChange(e.target.value)}
/>
<SelectGroup>
<Select name="type" value={type ?? "ALL"} onValueChange={handleTypeChange}>
<SelectTrigger size="minimal" width="full">
<SelectValue placeholder={"Select type"} className="ml-2 whitespace-nowrap p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph
variant="extra-small"
className="whitespace-nowrap pl-0.5 transition group-hover:text-text-bright"
>
All types
</Paragraph>
</SelectItem>
<SelectItem value={"declarative"}>
<ScheduleTypeCombo type="DECLARATIVE" className="text-xs text-text-dimmed" />
</SelectItem>
<SelectItem value={"imperative"}>
<ScheduleTypeCombo type="IMPERATIVE" className="text-xs text-text-dimmed" />
</SelectItem>
</SelectContent>
</Select>
</SelectGroup>
<SelectGroup>
<Select
name="environment"
@@ -0,0 +1,29 @@
import { ArchiveBoxIcon, ArrowsRightLeftIcon } from "@heroicons/react/20/solid";
import type { ScheduleType } from "@trigger.dev/database";
import { cn } from "~/utils/cn";
export function ScheduleTypeCombo({ type, className }: { type: ScheduleType; className?: string }) {
return (
<div className={cn("flex items-center space-x-1", className)}>
<ScheduleTypeIcon type={type} />
<span>{scheduleTypeName(type)}</span>
</div>
);
}
export function ScheduleTypeIcon({ type, className }: { type: ScheduleType; className?: string }) {
switch (type) {
case "IMPERATIVE":
return <ArrowsRightLeftIcon className={cn("size-4", className)} />;
case "DECLARATIVE":
return <ArchiveBoxIcon className={cn("size-4", className)} />;
}
}
export function scheduleTypeName(type: ScheduleType) {
switch (type) {
case "IMPERATIVE":
return "Imperative";
case "DECLARATIVE":
return "Declarative";
}
}
@@ -6,7 +6,7 @@ import {
import { CodeBlock } from "~/components/code/CodeBlock";
import { Callout } from "~/components/primitives/Callout";
import { DateTimeAccurate } from "~/components/primitives/DateTime";
import { Header2 } from "~/components/primitives/Headers";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
type SpanEventsProps = {
@@ -34,8 +34,8 @@ function SpanEventHeader({
}) {
return (
<div className="flex items-center justify-between">
<Header2 className={titleClassName}>{title}</Header2>
<Paragraph variant="small">
<Header3 className={titleClassName}>{title}</Header3>
<Paragraph variant="extra-small">
<DateTimeAccurate date={time} />
</Paragraph>
</div>
@@ -57,7 +57,7 @@ function SpanEvent({ spanEvent }: { spanEvent: OtelSpanEvent }) {
);
}
function SpanEventError({
export function SpanEventError({
spanEvent,
exception,
}: {
@@ -65,7 +65,7 @@ function SpanEventError({
exception: ExceptionEventProperties;
}) {
return (
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 p-3">
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
<SpanEventHeader
title={exception.type ?? "Error"}
time={spanEvent.time}
@@ -67,15 +67,19 @@ const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
EXPIRED: "Task has surpassed its ttl and won't be executed",
};
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY", "DELAYED"];
export const QUEUED_STATUSES = [
"PENDING",
"WAITING_FOR_DEPLOY",
"DELAYED",
] satisfies TaskRunStatus[];
export const RUNNING_STATUSES: TaskRunStatus[] = [
export const RUNNING_STATUSES = [
"EXECUTING",
"RETRYING_AFTER_FAILURE",
"WAITING_TO_RESUME",
];
] satisfies TaskRunStatus[];
export const FINISHED_STATUSES: TaskRunStatus[] = [
export const FINISHED_STATUSES = [
"COMPLETED_SUCCESSFULLY",
"CANCELED",
"COMPLETED_WITH_ERRORS",
@@ -83,7 +87,9 @@ export const FINISHED_STATUSES: TaskRunStatus[] = [
"SYSTEM_FAILURE",
"CRASHED",
"EXPIRED",
];
] satisfies TaskRunStatus[];
export type FINISHED_STATUSES = (typeof FINISHED_STATUSES)[number];
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
return taskRunStatusDescriptions[status];
@@ -1,14 +1,26 @@
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
import { StopIcon } from "@heroicons/react/24/outline";
import {
ArrowPathIcon,
ClockIcon,
CpuChipIcon,
RectangleStackIcon,
StopCircleIcon,
} from "@heroicons/react/20/solid";
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
import { useLocation } from "@remix-run/react";
import { formatDuration } from "@trigger.dev/core/v3";
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
import { useCallback, useRef } from "react";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Checkbox } from "~/components/primitives/Checkbox";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { Header3 } from "~/components/primitives/Headers";
import { useSelectedItems } from "~/components/primitives/SelectedItemsProvider";
import { useEnvironments } from "~/hooks/useEnvironments";
import { useFeatures } from "~/hooks/useFeatures";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder";
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
import { DateTime } from "../../primitives/DateTime";
@@ -25,14 +37,10 @@ import {
TableRow,
} from "../../primitives/Table";
import { CancelRunDialog } from "./CancelRunDialog";
import { LiveTimer } from "./LiveTimer";
import { ReplayRunDialog } from "./ReplayRunDialog";
import { TaskRunStatusCombo } from "./TaskRunStatus";
import { LiveTimer } from "./LiveTimer";
import { useSelectedItems } from "~/components/primitives/SelectedItemsProvider";
import { Checkbox } from "~/components/primitives/Checkbox";
import { useCallback, useRef } from "react";
import { run } from "@remix-run/dev/dist/cli/run";
import { formatNumber } from "~/utils/numberFormatter";
import { RunTag } from "./RunTag";
type RunsTableProps = {
total: number;
@@ -52,10 +60,14 @@ export function TaskRunsTable({
isLoading = false,
allowSelection = false,
}: RunsTableProps) {
const user = useUser();
const organization = useOrganization();
const project = useProject();
const checkboxes = useRef<(HTMLInputElement | null)[]>([]);
const { selectedItems, has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection);
const { isManagedCloud } = useFeatures();
const showCompute = user.admin && isManagedCloud;
const navigateCheckboxes = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>, index: number) => {
@@ -110,16 +122,122 @@ export function TaskRunsTable({
</TableHeaderCell>
)}
<TableHeaderCell alignment="right">Run #</TableHeaderCell>
<TableHeaderCell>Env</TableHeaderCell>
<TableHeaderCell>Task ID</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell>Env</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Started</TableHeaderCell>
<TableHeaderCell>Duration</TableHeaderCell>
<TableHeaderCell
colSpan={3}
tooltip={
<div className="flex max-w-xs flex-col gap-4 p-1">
<div>
<div className="mb-0.5 flex items-center gap-1.5">
<RectangleStackIcon className="size-4 text-text-dimmed" />
<Header3>Queued duration</Header3>
</div>
<Paragraph variant="small" className="!text-wrap text-text-dimmed">
The amount of time from when the run was created to it starting to run.
</Paragraph>
</div>
<div>
<div className="mb-0.5 flex items-center gap-1.5">
<ClockIcon className="size-4 text-blue-500" /> <Header3>Run duration</Header3>
</div>
<Paragraph variant="small" className="!text-wrap text-text-dimmed">
The total amount of time from the run starting to it finishing. This includes
all time spent waiting.
</Paragraph>
</div>
<div>
<div className="mb-0.5 flex items-center gap-1.5">
<CpuChipIcon className="size-4 text-success" />
<Header3>Compute duration</Header3>
</div>
<Paragraph variant="small" className="!text-wrap text-text-dimmed">
The amount of compute time used in the run. This does not include time spent
waiting.
</Paragraph>
</div>
</div>
}
>
Duration
</TableHeaderCell>
{showCompute && (
<>
<TableHeaderCell>Compute</TableHeaderCell>
</>
)}
<TableHeaderCell>Test</TableHeaderCell>
<TableHeaderCell>Created at</TableHeaderCell>
<TableHeaderCell>Delayed until</TableHeaderCell>
<TableHeaderCell>TTL</TableHeaderCell>
<TableHeaderCell
tooltip={
<div className="max-w-xs p-1">
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
When you want to trigger a task now, but have it run at a later time, you can use
the delay option.
</Paragraph>
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
Runs that are delayed and have not been enqueued yet will display in the dashboard
with a Delayed status.
</Paragraph>
<LinkButton
to={docsPath("v3/triggering")}
variant="tertiary/small"
LeadingIcon={BookOpenIcon}
>
Read docs
</LinkButton>
</div>
}
>
Delayed until
</TableHeaderCell>
<TableHeaderCell
tooltip={
<div className="max-w-xs p-1">
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
You can set a TTL (time to live) when triggering a task, which will automatically
expire the run if it hasnt started within the specified time.
</Paragraph>
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
All runs in development have a default ttl of 10 minutes. You can disable this by
setting the ttl option.
</Paragraph>
<LinkButton
to={docsPath("v3/triggering")}
variant="tertiary/small"
LeadingIcon={BookOpenIcon}
>
Read docs
</LinkButton>
</div>
}
>
TTL
</TableHeaderCell>
<TableHeaderCell
tooltip={
<div className="max-w-xs p-1">
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
You can add tags to a run and then filter runs using them.
</Paragraph>
<Paragraph variant="small" className="!text-wrap text-text-dimmed" spacing>
You can add tags when triggering a run or inside the run function.
</Paragraph>
<LinkButton
to={docsPath("v3/tags")}
variant="tertiary/small"
LeadingIcon={BookOpenIcon}
>
Read docs
</LinkButton>
</div>
}
>
Tags
</TableHeaderCell>
<TableHeaderCell>
<span className="sr-only">Go to page</span>
</TableHeaderCell>
@@ -127,7 +245,7 @@ export function TaskRunsTable({
</TableHeader>
<TableBody>
{total === 0 && !hasFilters ? (
<TableBlankRow colSpan={10}>
<TableBlankRow colSpan={14}>
{!isLoading && <NoRuns title="No runs found" />}
</TableBlankRow>
) : runs.length === 0 ? (
@@ -154,37 +272,67 @@ export function TaskRunsTable({
<TableCell to={path} alignment="right">
{formatNumber(run.number)}
</TableCell>
<TableCell to={path}>{run.taskIdentifier}</TableCell>
<TableCell to={path}>{run.version ?? ""}</TableCell>
<TableCell to={path}>
<EnvironmentLabel
environment={run.environment}
userName={run.environment.userName}
/>
</TableCell>
<TableCell to={path}>{run.taskIdentifier}</TableCell>
<TableCell to={path}>{run.version ?? ""}</TableCell>
<TableCell to={path}>
<TaskRunStatusCombo status={run.status} />
</TableCell>
<TableCell to={path}>
{run.startedAt ? <DateTime date={run.startedAt} /> : ""}
</TableCell>
<TableCell to={path}>
{run.startedAt && run.finishedAt ? (
formatDuration(new Date(run.startedAt), new Date(run.finishedAt), {
style: "short",
})
) : run.startedAt ? (
<LiveTimer startTime={new Date(run.startedAt)} />
) : (
""
)}
<TableCell to={path} className="w-[1%]" actionClassName="pr-0 tabular-nums">
<div className="flex items-center gap-1">
<RectangleStackIcon className="size-4 text-text-dimmed" />
{run.startedAt ? (
formatDuration(new Date(run.createdAt), new Date(run.startedAt), {
style: "short",
})
) : run.isCancellable ? (
<LiveTimer startTime={new Date(run.createdAt)} />
) : (
formatDuration(new Date(run.createdAt), new Date(run.updatedAt), {
style: "short",
})
)}
</div>
</TableCell>
<TableCell to={path} className="w-[1%]" actionClassName="px-4 tabular-nums">
<div className="flex items-center gap-1">
<ClockIcon className="size-4 text-blue-500" />
{run.startedAt && run.finishedAt ? (
formatDuration(new Date(run.startedAt), new Date(run.finishedAt), {
style: "short",
})
) : run.startedAt ? (
<LiveTimer startTime={new Date(run.startedAt)} />
) : (
""
)}
</div>
</TableCell>
<TableCell to={path} actionClassName="pl-0 tabular-nums">
<div className="flex items-center gap-1">
<CpuChipIcon className="size-4 text-success" />
{run.usageDurationMs > 0
? formatDurationMilliseconds(run.usageDurationMs, {
style: "short",
})
: ""}
</div>
</TableCell>
{showCompute && (
<TableCell to={path} className="tabular-nums">
{run.costInCents > 0 ? formatCurrencyAccurate(run.costInCents / 100) : ""}
</TableCell>
)}
<TableCell to={path}>
{run.isTest ? (
<CheckIcon className="h-4 w-4 text-charcoal-400" />
) : (
<StopIcon className="h-4 w-4 text-charcoal-850" />
)}
{run.isTest ? <CheckIcon className="h-4 w-4 text-charcoal-400" /> : ""}
</TableCell>
<TableCell to={path}>
{run.createdAt ? <DateTime date={run.createdAt} /> : ""}
@@ -193,6 +341,11 @@ export function TaskRunsTable({
{run.delayUntil ? <DateTime date={run.delayUntil} /> : ""}
</TableCell>
<TableCell to={path}>{run.ttl ?? ""}</TableCell>
<TableCell to={path} actionClassName="py-1">
<div className="flex gap-1">
{run.tags.map((tag) => <RunTag key={tag} tag={tag} />) || ""}
</div>
</TableCell>
<RunActionsCell run={run} path={path} />
</TableRow>
);
@@ -200,7 +353,7 @@ export function TaskRunsTable({
)}
{isLoading && (
<TableBlankRow
colSpan={8}
colSpan={14}
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-charcoal-900/90"
>
<Spinner /> <span className="text-text-dimmed">Loading</span>
@@ -260,7 +413,7 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
const organization = useOrganization();
const project = useProject();
const envs = useEnvironments();
if (isLoading) return <TableBlankRow colSpan={9}></TableBlankRow>;
if (isLoading) return <TableBlankRow colSpan={14}></TableBlankRow>;
const { environments, tasks, from, to, ...otherFilters } = filters;
@@ -273,7 +426,7 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
) {
const environment = envs?.find((env) => env.id === filters.environments[0]);
return (
<TableBlankRow colSpan={10}>
<TableBlankRow colSpan={14}>
<div className="py-14">
<Paragraph className="w-auto" variant="base/bright" spacing>
There are no runs for {filters.tasks[0]}
@@ -314,7 +467,7 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
}
return (
<TableBlankRow colSpan={10}>
<TableBlankRow colSpan={14}>
<div className="flex flex-col items-center justify-center gap-2">
<Paragraph className="w-auto" variant="small">
No runs currently match your filters. Try refreshing or modifying your filters.
@@ -0,0 +1,9 @@
<svg width="9" height="25" viewBox="0 0 9 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_13512_47994" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="9" height="25">
<rect width="9" height="25" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_13512_47994)">
<path d="M8.51694 0.5H10.5V24.5H8.51694C7.17088 24.5 5.94409 23.7281 5.36161 22.5146L1.69703 14.88C0.974863 13.3755 0.974863 11.6245 1.69703 10.12L5.3616 2.48544C5.94409 1.27194 7.17088 0.5 8.51694 0.5Z" fill="#1A1B1F" stroke="#272A2E" vectorEffect="non-scaling-stroke"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 598 B

+27 -1
View File
@@ -16,7 +16,6 @@ import {
} from "./components/primitives/OperatingSystemProvider";
import { getSharedSqsEventConsumer } from "./services/events/sqsEventConsumer";
import { singleton } from "./utils/singleton";
import { logger } from "./services/logger.server";
const ABORT_DELAY = 30000;
@@ -180,14 +179,41 @@ function logError(error: unknown, request?: Request) {
}
}
process.on("uncaughtException", (error, origin) => {
if (
error instanceof Prisma.PrismaClientKnownRequestError ||
error instanceof Prisma.PrismaClientUnknownRequestError
) {
// Don't exit the process if the error is a Prisma error
logger.error("uncaughtException prisma error", {
error,
prismaMessage: error.message,
code: "code" in error ? error.code : undefined,
meta: "meta" in error ? error.meta : undefined,
stack: error.stack,
origin,
});
} else {
logger.error("uncaughtException", {
error: { name: error.name, message: error.message, stack: error.stack },
origin,
});
}
process.exit(1);
});
const sqsEventConsumer = singleton("sqsEventConsumer", getSharedSqsEventConsumer);
export { apiRateLimiter } from "./services/apiRateLimit.server";
export { socketIo } from "./v3/handleSocketIo.server";
export { wss } from "./v3/handleWebsockets.server";
export { registryProxy } from "./v3/registryProxy.server";
export { runWithHttpContext } from "./services/httpAsyncStorage.server";
import { eventLoopMonitor } from "./eventLoopMonitor.server";
import { env } from "./env.server";
import { logger } from "./services/logger.server";
import { Prisma } from "./db.server";
if (env.EVENT_LOOP_MONITOR_ENABLED === "1") {
eventLoopMonitor.enable();
+6 -10
View File
@@ -97,8 +97,9 @@ const EnvironmentSchema = z.object({
* @example "1000ms"
* @example "1000s"
*/
API_RATE_LIMIT_WINDOW: z.string().default("60s"),
API_RATE_LIMIT_MAX: z.coerce.number().int().default(600),
API_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), // refill 250 tokens every 10 seconds
API_RATE_LIMIT_MAX: z.coerce.number().int().default(750), // allow bursts of 750 requests
API_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(250), // refix 250 tokens every 10 seconds
API_RATE_LIMIT_REQUEST_LOGS_ENABLED: z.string().default("0"),
API_RATE_LIMIT_REJECTION_LOGS_ENABLED: z.string().default("1"),
@@ -193,14 +194,9 @@ const EnvironmentSchema = z.object({
USAGE_EVENT_URL: z.string().optional(),
PROD_USAGE_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(),
CENTS_PER_HOUR_MICRO: z.coerce.number().default(0),
CENTS_PER_HOUR_SMALL_1X: z.coerce.number().default(0),
CENTS_PER_HOUR_SMALL_2X: z.coerce.number().default(0),
CENTS_PER_HOUR_MEDIUM_1X: z.coerce.number().default(0),
CENTS_PER_HOUR_MEDIUM_2X: z.coerce.number().default(0),
CENTS_PER_HOUR_LARGE_1X: z.coerce.number().default(0),
CENTS_PER_HOUR_LARGE_2X: z.coerce.number().default(0),
BASE_RUN_COST_IN_CENTS: z.coerce.number().default(0),
CENTS_PER_VCPU_SECOND: z.coerce.number().default(0),
CENTS_PER_GB_RAM_SECOND: z.coerce.number().default(0),
CENTS_PER_RUN: z.coerce.number().default(0),
USAGE_OPEN_METER_API_KEY: z.string().optional(),
USAGE_OPEN_METER_BASE_URL: z.string().optional(),
+10 -4
View File
@@ -18,13 +18,13 @@ export function useSearchParams() {
}
if (typeof value === "string") {
search.set(param, value);
search.set(param, encodeURIComponent(value));
continue;
}
search.delete(param);
for (const v of value) {
search.append(param, v);
search.append(param, encodeURIComponent(v));
}
}
},
@@ -54,14 +54,20 @@ export function useSearchParams() {
const value = useCallback(
(param: string) => {
return search.get(param) ?? undefined;
const val = search.get(param) ?? undefined;
if (val === undefined) {
return val;
}
return decodeURIComponent(val);
},
[location, search]
);
const values = useCallback(
(param: string) => {
return search.getAll(param);
const all = search.getAll(param);
return all.map((v) => decodeURIComponent(v));
},
[location, search]
);
+4
View File
@@ -13,3 +13,7 @@ interface Array<T> {
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
export type EnsureProperty<T, K extends keyof T> = T & {
[P in K]-?: NonNullable<T[P]>;
};
+1
View File
@@ -118,6 +118,7 @@ export async function adminGetOrganizations(userId: string, { page, search }: Se
id: true,
slug: true,
title: true,
v2Enabled: true,
v3Enabled: true,
members: {
select: {
+8 -2
View File
@@ -1,9 +1,15 @@
import { prisma } from "~/db.server";
import { createEnvironment } from "./organization.server";
export async function getTeamMembersAndInvites({ userId, slug }: { userId: string; slug: string }) {
export async function getTeamMembersAndInvites({
userId,
organizationId,
}: {
userId: string;
organizationId: string;
}) {
const org = await prisma.organization.findFirst({
where: { slug, members: { some: { userId } } },
where: { id: organizationId, members: { some: { userId } } },
select: {
members: {
select: {
@@ -77,7 +77,7 @@ export async function createOrganization(
}
export async function createEnvironment(
organization: Pick<Organization, "id">,
organization: Pick<Organization, "id" | "maximumConcurrencyLimit">,
project: Pick<Project, "id">,
type: RuntimeEnvironment["type"],
member?: OrgMember,
@@ -95,7 +95,7 @@ export async function createEnvironment(
pkApiKey,
shortcode,
autoEnableInternalSources: type !== "DEVELOPMENT",
maximumConcurrencyLimit: env.DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT,
maximumConcurrencyLimit: organization.maximumConcurrencyLimit / 3,
organization: {
connect: {
id: organization.id,
+11 -10
View File
@@ -4,17 +4,20 @@ import { prisma } from "~/db.server";
import type { Project } from "@trigger.dev/database";
import { Organization, createEnvironment } from "./organization.server";
import { env } from "~/env.server";
import { projectCreated } from "~/services/platform.v3.server";
export type { Project } from "@trigger.dev/database";
const externalRefGenerator = customAlphabet("abcdefghijklmnopqrstuvwxyz", 20);
type Options = {
organizationSlug: string;
name: string;
userId: string;
version: "v2" | "v3";
};
export async function createProject(
{
organizationSlug,
name,
userId,
version,
}: { organizationSlug: string; name: string; userId: string; version: "v2" | "v3" },
{ organizationSlug, name, userId, version }: Options,
attemptCount = 0
): Promise<Project & { organization: Organization }> {
//check the user has permissions to do this
@@ -87,14 +90,12 @@ export async function createProject(
// Create the dev and prod environments
await createEnvironment(organization, project, "PRODUCTION");
if (version === "v2") {
await createEnvironment(organization, project, "STAGING");
}
for (const member of project.organization.members) {
await createEnvironment(organization, project, "DEVELOPMENT", member);
}
await projectCreated(organization, project);
return project;
}
@@ -116,7 +116,7 @@ export async function disconnectSession(environmentId: string) {
return session;
}
type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{
export type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{
select: {
id: true;
type: true;
+21 -1
View File
@@ -1,5 +1,6 @@
import type { JobRun, Task, TaskAttempt } from "@trigger.dev/database";
import type { JobRun, Task, TaskAttempt, TaskTriggerSource } from "@trigger.dev/database";
import { CachedTask, ServerTask } from "@trigger.dev/core";
import { PrismaClientOrTransaction, sqlDatabaseSchema } from "~/db.server";
export type TaskWithAttempts = Task & {
attempts: TaskAttempt[];
@@ -116,3 +117,22 @@ function prepareTaskForCaching(task: TaskForCaching): CachedTask {
function calculateCachedTaskSize(task: CachedTask): number {
return JSON.stringify(task).length;
}
/**
*
* @param prisma An efficient query to get all task identifiers for a project.
* It has indexes for fast performance.
* It does NOT care about versions, so includes all tasks ever created.
*/
export function getAllTaskIdentifiers(prisma: PrismaClientOrTransaction, projectId: string) {
return prisma.$queryRaw<
{
slug: string;
triggerSource: TaskTriggerSource;
}[]
>`
SELECT DISTINCT(slug), "triggerSource"
FROM ${sqlDatabaseSchema}."BackgroundWorkerTask"
WHERE "projectId" = ${projectId}
ORDER BY slug ASC;`;
}
@@ -0,0 +1,42 @@
import { prisma } from "~/db.server";
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
export const MAX_TAGS_PER_RUN = 5;
export async function createTag({ tag, projectId }: { tag: string; projectId: string }) {
if (tag.trim().length === 0) return;
return prisma.taskRunTag.upsert({
where: {
projectId_name: {
projectId: projectId,
name: tag,
},
},
create: {
name: tag,
friendlyId: generateFriendlyId("runtag"),
projectId: projectId,
},
update: {},
});
}
export async function getTagsForRunId({
friendlyId,
environmentId,
}: {
friendlyId: string;
environmentId: string;
}) {
const run = await prisma.taskRun.findFirst({
where: {
friendlyId,
runtimeEnvironmentId: environmentId,
},
select: {
tags: true,
},
});
return run?.tags ?? undefined;
}
+50 -12
View File
@@ -1,3 +1,4 @@
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
import type {
CronItem,
CronItemOptions,
@@ -11,20 +12,19 @@ import type {
WorkerUtils,
} from "graphile-worker";
import {
Logger as GraphileLogger,
run as graphileRun,
makeWorkerUtils,
parseCronItems,
Logger as GraphileLogger,
} from "graphile-worker";
import { SpanKind, trace } from "@opentelemetry/api";
import { flattenAttributes } from "@trigger.dev/core/v3";
import omit from "lodash.omit";
import { z } from "zod";
import { $replica, PrismaClient, PrismaClientOrTransaction } from "~/db.server";
import { env } from "~/env.server";
import { PgListenService } from "~/services/db/pgListen.server";
import { workerLogger as logger } from "~/services/logger.server";
import { flattenAttributes } from "@trigger.dev/core/v3";
import { env } from "~/env.server";
const tracer = trace.getTracer("zodWorker", "3.0.0.dp.1");
@@ -338,11 +338,45 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
}
}
const { job, durationInMs } = await this.#addJob(
identifier as string,
payload,
spec,
options?.tx ?? this.#prisma
const { job, durationInMs } = await tracer.startActiveSpan(
`Enqueue ${identifier as string}`,
{
kind: SpanKind.PRODUCER,
attributes: {
"job.task_identifier": identifier as string,
"job.payload": payload,
"job.priority": spec.priority,
"job.run_at": spec.runAt?.toISOString(),
"job.jobKey": spec.jobKey,
"job.flags": spec.flags,
"job.max_attempts": spec.maxAttempts,
"worker.name": this.#name,
},
},
async (span) => {
try {
const results = await this.#addJob(
identifier as string,
payload,
spec,
options?.tx ?? this.#prisma
);
return results;
} catch (error) {
if (error instanceof Error) {
span.recordException(error);
} else {
span.recordException(new Error(String(error)));
}
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
}
);
logger.debug("Enqueued worker task", {
@@ -401,6 +435,12 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
const rows = AddJobResultsSchema.safeParse(results);
if (!rows.success) {
logger.debug("results returned from add_job could not be parsed", {
identifier,
payload,
spec,
});
throw new Error(
`Failed to add job to queue, zod parsing error: ${JSON.stringify(rows.error)}`
);
@@ -422,9 +462,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
const job = AddJobResultsSchema.safeParse(result);
if (!job.success) {
logger.debug("results returned from remove_job could not be parsed", {
error: job.error.flatten(),
result,
logger.debug("could not remove job, job_key did not exist", {
jobKey,
});
@@ -25,7 +25,7 @@ export class ApiRunPresenter {
}: ApiRunOptions) {
const take = Math.min(maxTasks, 50);
return await prisma.jobRun.findUnique({
return await prisma.jobRun.findFirst({
where: {
id: runId,
},

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