Compare commits

...

371 Commits

Author SHA1 Message Date
Eric Allam 17a117d1ee Merge pull request #126 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-06-28 16:53:29 +01:00
github-actions[bot] bc43cc232d chore: Update version for release (next) 2023-06-28 15:40:08 +00:00
Eric Allam 94ae9e65f4 Fixed an issue in @trigger.dev/init that cause environment variables to be incorrectly added to .env.local 2023-06-28 16:38:51 +01:00
James Ritchie 2ed7158cf1 Fleshed out the ngrok section some more 2023-06-28 16:14:41 +01:00
James Ritchie 9d8a21a3de Added an ngrok image 2023-06-28 16:14:41 +01:00
Eric Allam d313eaa742 Update the performRunExecution import 2023-06-28 15:50:02 +01:00
Eric Allam c5d91e7cca Adding the resend package and fixing issues with the webapp 2023-06-28 15:48:23 +01:00
James Ritchie cc668a1624 Added a refresh page button 2023-06-28 15:34:31 +01:00
James Ritchie d17173ec0a Added another namedIcon 2023-06-28 15:34:21 +01:00
James Ritchie ebcd886d20 More Jobs help content 2023-06-28 15:15:48 +01:00
James Ritchie 4976bd1889 Show a callout link to the docs when you have 0 or >0 Jobs 2023-06-28 15:15:48 +01:00
James Ritchie 9da6d92fb3 useEnvironments hook now works as intended 2023-06-28 15:15:48 +01:00
Eric Allam 9dee5429d5 Move error formatting to a server file 2023-06-28 14:28:11 +01:00
James Ritchie ebe90aac5c WIP writing the setup Project help text 2023-06-28 14:08:35 +01:00
James Ritchie 289d69c79d Show 3 different CTAs for creating a Job depending on how many Jobs you currently have 2023-06-28 14:08:35 +01:00
James Ritchie 5bdaf4f662 Added a new namedIcon 2023-06-28 14:08:35 +01:00
James Ritchie 67f47bc3df Removed the Jobs skeleton and added a dashed outline version 2023-06-28 14:08:35 +01:00
James Ritchie 67ddd76ff3 Optionally set an isOpen conditional for when the Help panel is toggled open 2023-06-28 14:08:35 +01:00
James Ritchie 95de883879 Updated the terms links 2023-06-28 14:08:35 +01:00
James Ritchie 3227da122e If the integration connection is local, don’t show the irrelevant tabs 2023-06-28 14:08:35 +01:00
Eric Allam ae243c783a Merge pull request #125 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-06-28 14:01:17 +01:00
github-actions[bot] eb3b7f5146 chore: Update version for release (next) 2023-06-28 12:31:20 +00:00
Eric Allam 414439cac9 Upgrade to Remix 1.18.0 and try out the new dev server 2023-06-28 13:29:46 +01:00
Eric Allam 7dacccb70e Tag the main html title with the app env 2023-06-28 13:16:15 +01:00
Eric Allam a5e1d0595e Trigger a test deploy if a push to main happens 2023-06-28 12:13:44 +01:00
Eric Allam 7e8c6e92a2 Fixed seeding the cloud user github integration connection 2023-06-28 12:07:47 +01:00
Eric Allam 24542d4e21 Adding support for trigger source in the run context 2023-06-28 12:07:47 +01:00
Eric Allam b975125af5 Merge pull request #124 from triggerdotdev/changeset-release/main 2023-06-27 19:36:36 +01:00
github-actions[bot] cc8c60605b chore: Update version for release (next) 2023-06-27 16:43:11 +00:00
Eric Allam 23adc15ce3 Fixed the broken export issue with two of the new github tasks 2023-06-27 17:41:49 +01:00
Eric Allam 7204aa0c9a Push commit hash image tag to ECR
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-06-27 16:37:39 +01:00
Eric Allam 6750429cd4 Fixed the slack env vars for oauth2User 2023-06-27 16:24:10 +01:00
James Ritchie cfe0a6cbb8 Copy changes 2023-06-27 15:44:46 +01:00
James Ritchie a9f95971e8 Added an integration button image 2023-06-27 15:44:46 +01:00
James Ritchie d7eee844b7 Callout now has a docs variant which can take an href 2023-06-27 15:44:46 +01:00
James Ritchie 4c52731d16 Exported the integration icon to use in the help text 2023-06-27 15:44:46 +01:00
James Ritchie e53447d0bf Replaced all the inline help content on the route pages with components 2023-06-27 15:44:46 +01:00
James Ritchie 53ed23ac58 Initial setup for localising the help content into one component 2023-06-27 15:44:46 +01:00
Eric Allam 226fb4a349 Fallback to using the url passed in if the appHostEnvName isn’t in the environment 2023-06-27 15:30:01 +01:00
Eric Allam d89632eb9d Create latest and tagged images if in a versioned tag 2023-06-27 14:11:16 +01:00
Eric Allam 4621196993 Revert "Try publishing to ECR using good ole fashioned aws credentials"
This reverts commit 988e753c74.
2023-06-27 13:52:43 +01:00
Eric Allam 988e753c74 Try publishing to ECR using good ole fashioned aws credentials 2023-06-27 13:51:20 +01:00
Eric Allam aa49b2ca76 Do docker commands manually in publish.yml 2023-06-27 13:37:52 +01:00
Eric Allam fdcd2fdb27 Try removing logging in to the Github Container registry 2023-06-27 13:13:40 +01:00
Eric Allam 62f94206bb Fixed CLI showing undefined when not specifying a trigger-url flag 2023-06-27 13:00:06 +01:00
Eric Allam 7b6647cfd4 Fixed syntax error with publish.yml 2023-06-27 12:44:50 +01:00
Eric Allam 73828c5db7 Add a docker/login-action for logging into ECR 2023-06-27 12:36:03 +01:00
Eric Allam e1d942bb3a Add pnpm run db:studio command to open prisma studio 2023-06-27 11:27:31 +01:00
Eric Allam a764cf64b3 Remove docker hub container push and try to fix ECR 2023-06-27 11:20:16 +01:00
Eric Allam 018d51993a Need to login to ECR before pushing 2023-06-27 10:30:11 +01:00
Eric Allam 4ce51291de Try pushing to a private ECR registry 2023-06-27 09:56:34 +01:00
Eric Allam e0cd68787d Adding publishing docker images to a public ECR registry 2023-06-27 09:40:39 +01:00
James Ritchie 6601f4c505 Deleted empty old folders and files 2023-06-26 17:59:15 +01:00
James Ritchie 6f62f0e2f1 Updated docs links to the new docs URL 2023-06-26 16:53:56 +01:00
James Ritchie c00026e203 Fixed some issues with the developing readme guide 2023-06-26 16:43:55 +01:00
James Ritchie 38ac365953 Added a database .env example file 2023-06-26 16:40:30 +01:00
James Ritchie 7cfb0f7d2d Improvements to the readme, including better headings 2023-06-26 16:17:51 +01:00
James Ritchie bae194f366 Updated the README to include a development guide 2023-06-26 15:40:17 +01:00
Eric Allam 398d95528d Adding back in multiplatform support 2023-06-26 15:33:05 +01:00
Eric Allam d1b2ee5490 Remove multi-platform for now to test something 2023-06-26 15:20:22 +01:00
James Ritchie d39b9bc060 Made the default port 3030 2023-06-26 15:10:10 +01:00
James Ritchie 0c0b7d86ee Deleted old infisical files 2023-06-26 15:10:10 +01:00
Eric Allam 03c4d9504f Cleanup the docker compose services stuff 2023-06-26 15:09:31 +01:00
Eric Allam 0737bfc5fe Add QEMU for multi-arch builds 2023-06-26 14:45:21 +01:00
James Ritchie 80b4916094 docker .env links to root .env 2023-06-26 14:45:03 +01:00
James Ritchie 00845b43b4 Renamed the .example env file and added instructions for getting your Resend API 2023-06-26 14:31:27 +01:00
James Ritchie 83a74808d5 Added an example .env 2023-06-26 13:30:34 +01:00
Eric Allam 951679cc10 Fix pnpm i 2023-06-26 12:10:55 +01:00
D-K-P 3cd95f06b2 Added new companyicons 2023-06-26 11:54:15 +01:00
Eric Allam de00fa91ae Generate db client before typecheck 2023-06-26 11:53:31 +01:00
Eric Allam 25d4f0b347 Fixed CLI typescript errors 2023-06-26 11:49:01 +01:00
Eric Allam 413aa28594 Fixed typescript error 2023-06-26 11:45:50 +01:00
Eric Allam 29e153bc54 Fixed pnpm lock file OUTDATED 2023-06-26 11:42:35 +01:00
Eric Allam 7b1a2f60d0 Adding changesets workflow to automatically version/release package changes 2023-06-26 11:41:40 +01:00
Matt Aitken 2302626703 Added line highlighting to the API Key code samples 2023-06-26 11:18:26 +01:00
Matt Aitken 28b3193743 Docs links use the pathBuilder 2023-06-26 11:18:26 +01:00
Eric Allam 0fdf8d0f6a Another fix for the CI docker build caching 2023-06-26 11:17:09 +01:00
D-K-P 890c93a820 Added Resend 2023-06-26 11:16:35 +01:00
Eric Allam 3bfcde99dc Update LICENSE to Apache 2.0 2023-06-26 10:42:07 +01:00
Eric Allam d5a8d19b66 Trying to fix the cache 2023-06-26 10:18:39 +01:00
Eric Allam 7a3a9a6f7c CI: Docker improvements
- Multiple arches in the docker image
- Use the docker@metadata-action github action to create the tags and labels
- Push to Docker Hub and Github Container Repository
2023-06-24 10:15:52 +01:00
Eric Allam a816c52699 Update docs link 2023-06-23 17:32:10 +01:00
Eric Allam bdd3157a45 Merge branch 'main' into dev-q22023 2023-06-23 17:27:48 +01:00
Eric Allam 7337e75d1e Add link to the fly.io repo 2023-06-23 17:10:40 +01:00
Eric Allam b39f1694c3 Fixed the version of the @trigger.dev/nextjs package 2023-06-23 17:02:39 +01:00
Eric Allam b3a919293c Update the devDependency on nextjs to be more permissive 2023-06-23 16:59:55 +01:00
D-K-P 927eb1d0ba changed green to toxic 2023-06-23 15:27:07 +01:00
D-K-P b812c6d984 Updated gradient 2023-06-23 15:25:16 +01:00
Eric Allam 6a768c3f84 Create LICENSE 2023-06-23 15:11:00 +01:00
Eric Allam 83f4e65bc3 Remove license and update README 2023-06-23 15:08:37 +01:00
Eric Allam ad33abe6bc Use the REMIX_APP_PORT for the Remix app server port when developing locally 2023-06-23 14:53:41 +01:00
Eric Allam 2e1624235c Improved the CLI prompts 2023-06-23 14:41:58 +01:00
Eric Allam 647f09f2f3 Remove staging from flightcontrol.json and only deploy prod on pushes to v1 branch 2023-06-23 12:32:35 +01:00
Eric Allam d0ad2bd38b Fixed the init help text 2023-06-23 12:22:35 +01:00
Eric Allam 3ae4e11bbe Changeset: version 2023-06-23 12:20:20 +01:00
Eric Allam 6f19a3e750 Fix for the init CLI for next.js projects that use the src directory 2023-06-23 12:20:06 +01:00
Eric Allam e8442eedad Commenting these out for now because they break build 2023-06-23 11:59:30 +01:00
Eric Allam a606db25f6 Changeset: version 2023-06-23 11:41:11 +01:00
Eric Allam 82f715bab6 Removed cli package from todo-example 2023-06-23 11:40:53 +01:00
Eric Allam 28914b878c Adding the @trigger.dev/init CLI package 2023-06-23 11:40:26 +01:00
Eric Allam 3c2f1709cc Remove welcome email (need to add back in for cloud) 2023-06-22 22:57:09 +01:00
Eric Allam 40ffde33b6 Add isManagedCloud feature, remove sentry, and require custom oauth app on self-hosting 2023-06-22 22:48:57 +01:00
Eric Allam 8184c21a80 Build the oauth2 callback url from the server (and use env.APP_ORIGIN) 2023-06-22 21:54:01 +01:00
Eric Allam 5c13d0c2df Fixed pnpm lock file again 2023-06-22 21:37:09 +01:00
Eric Allam fd8fd22bdc Fixed https missing from url 2023-06-22 21:35:49 +01:00
Eric Allam 31d8669c9d Make github auth optional and better name the env vars 2023-06-22 17:42:51 +01:00
D-K-P 59e1fbf565 Removed import 2023-06-22 17:10:10 +01:00
D-K-P 80c9bb0317 Added new components to the to do page and lots of improvements 2023-06-22 17:09:42 +01:00
Matt Aitken 6c71bad8ef Update Job in app folder to use owner + repo 2023-06-22 17:08:10 +01:00
Eric Allam 0e82f6287a Try and fix the fly entrypoint issues 2023-06-22 16:54:14 +01:00
Matt Aitken 52aacc1e1d OpenAI: createEmbedding 2023-06-22 16:29:00 +01:00
Eric Allam 909b4a5432 More fly work 2023-06-22 16:26:55 +01:00
Matt Aitken b48b724c01 OpenAI: createImage 2023-06-22 16:12:02 +01:00
Matt Aitken 91ac1567d4 OpenAI: createEdit 2023-06-22 16:12:02 +01:00
Eric Allam c034035aba Working on deploying to fly 2023-06-22 16:09:08 +01:00
Matt Aitken cd852f9563 OpenAI: retrieveModel 2023-06-22 15:51:31 +01:00
Matt Aitken 010ac6e5b6 OpenAI tasks: createFineTune, listFineTunes, retrieveFineTune, cancelFineTune, listFineTuneEvents, deleteFineTune 2023-06-22 15:45:09 +01:00
Eric Allam 0a3cf46068 Try this for publishing public images 2023-06-22 15:17:27 +01:00
Matt Aitken 2bbeeb860d Fixed gitignore for tmp folders 2023-06-22 15:10:19 +01:00
Matt Aitken b74045fe55 Ignore tmp folders 2023-06-22 15:10:19 +01:00
Matt Aitken a940102d83 Max lines before scrolling on the task input is 35 lines 2023-06-22 15:10:19 +01:00
Matt Aitken 74e9d5eb3c New OpenAI tasks: listModels, createFile, listFiles, createFineTuneFile 2023-06-22 15:10:19 +01:00
Matt Aitken b256d66374 Subtasks should always be set to last=false so you get the line at the bottom 2023-06-22 15:10:19 +01:00
Matt Aitken 5b23e60b04 Created an OpenAI example file 2023-06-22 15:10:19 +01:00
Eric Allam 4cd71decbc Starting to build fly demo 2023-06-22 15:03:13 +01:00
Eric Allam 1ba372e2c3 Unbreak the build 2023-06-22 14:33:08 +01:00
Eric Allam d88f8c87f6 Don’t cancel in progress runs 2023-06-22 14:31:52 +01:00
D-K-P ff02ab2aa0 Updated pnpm 2023-06-22 14:29:33 +01:00
D-K-P 38c71bdb3b Added logo / to do icon and style updates 2023-06-22 14:29:33 +01:00
Eric Allam b68ce9695c Try and fix the cache-from cause of Adam 2023-06-22 14:28:15 +01:00
Eric Allam 0d3e7bd996 Use docker/build-push-action 2023-06-22 14:17:30 +01:00
Eric Allam 1e0acf4ce8 Add back in the build:server script to webapp 2023-06-22 14:05:47 +01:00
Eric Allam b9e8e084a8 chmod the wait-for-it script 2023-06-22 13:54:47 +01:00
Eric Allam 1701d4253d Remove prisma config in package.json 2023-06-22 13:53:46 +01:00
Eric Allam ff60b71321 Quiet npx output 2023-06-22 13:48:19 +01:00
Eric Allam f68439b936 Adopt buildjet for faster CI builds 2023-06-22 13:46:34 +01:00
D-K-P a3fe3944b7 Style updates 2023-06-22 13:37:03 +01:00
D-K-P 9cf44777e1 Added to do custom input 2023-06-22 13:37:03 +01:00
D-K-P 5206c2c971 Removed shadcn input 2023-06-22 13:37:03 +01:00
Eric Allam 294fd10304 Use pnpm 7.18 in the docker workflow 2023-06-22 13:36:00 +01:00
Eric Allam 32650f8b9a Fix lockfile 2023-06-22 13:34:27 +01:00
Eric Allam c5d96db9a8 WIP docker github workflow 2023-06-22 13:11:43 +01:00
D-K-P d04c66006b Updated companyicons package 2023-06-22 12:03:13 +01:00
Matt Aitken d43529dce9 Added some GitHub property helpers 2023-06-22 12:02:08 +01:00
Matt Aitken 5ba95f3cbc Link to the run trigger, so it’s selected 2023-06-22 12:02:08 +01:00
Matt Aitken 7381e5b393 GitHub addIssueAssignees and addIssueLabels 2023-06-22 12:02:08 +01:00
Eric Allam b62f00dfd7 Implemented seeding the cloud based on env vars 2023-06-22 12:00:18 +01:00
Matt Aitken 95787fcc66 Make the tasks user a separate owner and repo too 2023-06-22 10:49:50 +01:00
Matt Aitken 4ef76f72dc Separate GitHub repo into owner and repo, to match official SDK/API 2023-06-22 10:49:50 +01:00
Matt Aitken 939f33e791 Added scrolling to the Jobs menu (I have a ton) 2023-06-22 10:49:50 +01:00
D-K-P e15ade91d4 Added Trigger sync card and made lots of general improvements 2023-06-21 21:54:53 +01:00
Matt Aitken 0776cc884d Pull request review 2023-06-21 19:50:20 +01:00
Matt Aitken 0ad09eb11f Pull request event 2023-06-21 19:41:33 +01:00
Matt Aitken 9e8b37c6cf Added push event 2023-06-21 19:29:02 +01:00
Matt Aitken c2839934b4 Added branch and tag events 2023-06-21 19:04:14 +01:00
Matt Aitken e2e3c3a9b3 Latest lock file 2023-06-21 19:04:03 +01:00
Matt Aitken 9b3f4bc1a3 Next generates a weird .next/package.json when running, so need to restrict example workspaces 2023-06-21 19:03:53 +01:00
Matt Aitken cf9c34950f Show more of the comment body 2023-06-21 18:44:08 +01:00
Matt Aitken d4a2927279 Fix for integration-kit issues 2023-06-21 18:43:58 +01:00
Matt Aitken 56172b1e5c Star examples 2023-06-21 18:24:38 +01:00
Matt Aitken 7def5437fd Try and bundle up the integration-kit with the github package 2023-06-21 18:24:38 +01:00
D-K-P d21e364f11 Added trigger cards wip 2023-06-21 18:05:26 +01:00
D-K-P 7c6440fd9a Added cards / input / button / accordian and updated page 2023-06-21 18:05:04 +01:00
Matt Aitken 0b61207ecf Latest installs 2023-06-21 17:55:16 +01:00
Matt Aitken fff2f40924 Added issue commented example job 2023-06-21 17:55:16 +01:00
Matt Aitken 2166929fa8 Issue commented event, not working properly because of truncate function issue 2023-06-21 17:55:16 +01:00
Matt Aitken 5aa53da864 Next.js project using the latest eslint setup that Vercel recommend 2023-06-21 17:55:16 +01:00
Matt Aitken 21d7ff2f74 Added a function to truncate a string with an ellipsis 2023-06-21 17:55:16 +01:00
Eric Allam 8bba30e290 Initial Docker and docker compose setup 2023-06-21 17:07:17 +01:00
Eric Allam fd94a32b95 Removing a bunch of old stuff 2023-06-21 17:07:17 +01:00
Matt Aitken 25f9aa8c2d Started fleshing out GitHub events with examples and more events 2023-06-21 17:00:29 +01:00
Matt Aitken 172bb4527c Export the EventSpecificationExample type so it can be used in integrations 2023-06-21 17:00:15 +01:00
Matt Aitken 8ad769569c Spaced the properties out a bit vertically 2023-06-21 16:59:55 +01:00
Matt Aitken 32eba22628 Remove old oauth files 2023-06-21 16:59:32 +01:00
Matt Aitken 8d4179b5df Improved the coming soon billing description 2023-06-21 14:49:20 +01:00
Matt Aitken b7385825b6 Added missing auth param to GitHub integration 2023-06-21 14:11:52 +01:00
Matt Aitken 5a20528cc8 @trigger.dev/github package now uses integration-kit 2023-06-21 14:02:16 +01:00
Matt Aitken 324cf161ec Added @trigger.dev/integration-kit package 2023-06-21 14:02:16 +01:00
D-K-P 9f84e77f57 Replaced the fire icon with the td triangle on the integrations page 2023-06-21 13:56:11 +01:00
Matt Aitken beaf0a9ecb Coming soon is now nicer 2023-06-21 13:09:46 +01:00
Matt Aitken fb308e8038 Sent a notification when the feedback form is submitted, even if that custoemr is already active 2023-06-21 13:04:38 +01:00
Matt Aitken d2d15dae9f Added coming soon pages 2023-06-21 13:04:26 +01:00
Matt Aitken 977f064157 Added “background” icon 2023-06-21 10:53:46 +01:00
Matt Aitken 8d23dd0186 Set the user id in Plain 2023-06-21 10:53:41 +01:00
Matt Aitken 90ae06df2a Improved the feedback form 2023-06-21 10:33:01 +01:00
D-K-P 2cf253a914 Added new integrations / icons 2023-06-21 10:18:59 +01:00
D-K-P 9e462cc329 Minor updates 2023-06-21 10:17:59 +01:00
Matt Aitken 36ae8910b6 Added the help type 2023-06-20 22:45:09 +01:00
Matt Aitken fe0ed0bc9a The Plain feedback form is working 2023-06-20 22:43:33 +01:00
Matt Aitken ec738515b7 Initial work on feedback form 2023-06-20 22:21:36 +01:00
Matt Aitken 287b6c6bc7 Remove user agent/platform logging 2023-06-20 21:04:09 +01:00
Matt Aitken aad93b9147 Updated the replacement data format 2023-06-20 20:15:47 +01:00
Matt Aitken 31176245bd Changed the format slightly, added timestamps as further examples 2023-06-20 20:15:39 +01:00
Matt Aitken da45c7e2b9 Added support for replacements in examples, added __CURRENT_DATE__ to start 2023-06-20 20:11:06 +01:00
Matt Aitken 3c73cb3fed Example payloads are now any so we can do replacements 2023-06-20 20:10:34 +01:00
Matt Aitken 275d01421e Improved Windows shortcut modifier display 2023-06-20 17:51:19 +01:00
Matt Aitken 75edfccc31 Shortcut keys are working 2023-06-20 17:45:24 +01:00
Matt Aitken 1d230d9fed Nicely formatted shortcut in buttons 2023-06-20 15:17:29 +01:00
Matt Aitken 7d87dbf2ed Updated packages 2023-06-20 14:48:10 +01:00
Matt Aitken 44467331a2 Improved the ShortcutKey display 2023-06-20 14:47:47 +01:00
Matt Aitken 29cbfd4fb3 Use ClipboardField for Integration package installation 2023-06-20 13:48:34 +01:00
Matt Aitken d946408105 Integration sheet: show “View docs” button and the description 2023-06-20 13:48:21 +01:00
Matt Aitken 6101372ba8 Added OpenAI integration 2023-06-20 13:48:04 +01:00
Matt Aitken 4993860a18 Added optional description to an integration 2023-06-20 13:47:56 +01:00
Matt Aitken 5e29119e83 You can now only show integrations in the integrations list 2023-06-20 13:38:54 +01:00
Matt Aitken fe14664cfd Moved the logo-icon to be in alphabetical order 2023-06-20 13:38:54 +01:00
Matt Aitken f871817a2f Use geometricPrecision on Company icon svgs 2023-06-20 13:38:54 +01:00
Matt Aitken 6ccacaa92e Switch labels now supported and more complete styling 2023-06-20 13:38:54 +01:00
D-K-P 24e0f04aca Updated pnpm-lock file 2023-06-20 13:34:32 +01:00
D-K-P f878d499a7 Added ToDoRow component 2023-06-20 13:34:02 +01:00
D-K-P c24cd1c02b Added HeroIcons 2023-06-20 13:34:01 +01:00
D-K-P c6028fca44 Updated Paragraph and TextStyling 2023-06-20 13:33:45 +01:00
Matt Aitken 25daa5b358 Added Switch component with small and large variants 2023-06-20 12:20:03 +01:00
Matt Aitken 0503a2a517 Moved ClipboardField into primities 2023-06-20 12:04:49 +01:00
Matt Aitken 28fd3d9b18 Fix for re-runs linking to the incorrect detail panels 2023-06-20 12:00:17 +01:00
Matt Aitken 1a9af1dee4 Fix for Retry table not having key and header row 2023-06-20 11:55:28 +01:00
Matt Aitken ce27de1af6 Removed todo that has been done 2023-06-20 11:55:16 +01:00
Matt Aitken c9d5334a38 Improved the style of the “Rerun job” popover 2023-06-20 11:51:03 +01:00
Matt Aitken bd20b7b8e0 Show the event name in the Run Trigger cell on the left 2023-06-20 11:39:44 +01:00
Matt Aitken 52f40c8c26 Only show the retries table if there was > 1 2023-06-20 11:33:05 +01:00
Matt Aitken 622dd6aba4 Show a table of Task retries if there were some 2023-06-20 11:30:07 +01:00
Matt Aitken 969a6a1f2c Fix for the final retry 2023-06-20 11:29:51 +01:00
Matt Aitken 08eab9622d Improved the webhook instruction text 2023-06-20 10:41:37 +01:00
Matt Aitken 107960b67c Endpoint sheet shows the webhook URL 2023-06-20 10:40:01 +01:00
Matt Aitken b394f5e43f ClipboardField works when full width, has scrolling if the content is too large 2023-06-20 10:39:43 +01:00
Matt Aitken 4d71a5c9d4 Refreshing endpoints is working 2023-06-20 10:26:35 +01:00
Matt Aitken 59e2216861 Streaming working for the endpoint sheet 2023-06-20 10:26:35 +01:00
Matt Aitken 2fba5da04e sse function makes it much easier to do ServerSentEvents 2023-06-20 10:26:35 +01:00
Matt Aitken efbe2ddbf7 The AbortController approach causes the stream endpoint to be called all the time 2023-06-20 10:26:35 +01:00
Matt Aitken 7f353723eb Improved the environments form/page 2023-06-20 10:26:35 +01:00
Matt Aitken a94d50f3cd Made the sheet wider 2023-06-20 10:26:35 +01:00
Eric Allam ae66e6f01a Run pnpm i 2023-06-19 17:22:53 +01:00
Eric Allam e4b0b1e305 Add support for background task operations, starting with fetch 2023-06-19 17:22:18 +01:00
D-K-P 65ca2560fc Started adding the first col to the page 2023-06-19 17:19:16 +01:00
D-K-P bdbf04efe9 Added clsx / tailwind-merge to the package.json 2023-06-19 17:18:55 +01:00
D-K-P 4b56671d2d Added tailwind config / clsx / tailwind-merge 2023-06-19 17:18:35 +01:00
D-K-P f2b190b21a Replaced to-do folder with a blank nextjs project 2023-06-19 16:38:30 +01:00
D-K-P 4f5cf931be Duplicated the nextjs example folder for the to-do app 2023-06-19 15:28:03 +01:00
D-K-P 68beefdaf2 Added new integration logos and upped the companyicons package number 2023-06-19 15:05:15 +01:00
Matt Aitken 4a52fd2edc Added errors in the form if creating the endpoint fails 2023-06-19 12:17:58 +01:00
Matt Aitken 47fdfb344d Progress displaying endpoints and editing them 2023-06-19 11:48:33 +01:00
Matt Aitken ebbbc85d7b Added a success Callout variant 2023-06-19 11:48:14 +01:00
Matt Aitken 22ff2c36d9 Open a sheet when you click a row 2023-06-16 19:33:40 +01:00
Matt Aitken 5db2a1bb84 Improved the spacing 2023-06-16 19:19:10 +01:00
Matt Aitken 8a4ae0df20 WIP on the Environment clients 2023-06-16 19:17:30 +01:00
Matt Aitken 80c3e48f5c Tables can now have onClick events 2023-06-16 19:17:11 +01:00
Matt Aitken a7244032e4 Updated the variant for the CLipboardField 2023-06-16 16:37:50 +01:00
Matt Aitken 7fc3ff26bc Rejigged database migrate deploy scripts 2023-06-16 16:30:35 +01:00
Matt Aitken 0648d230fc WIP on environment page 2023-06-16 16:30:35 +01:00
Matt Aitken 2626608200 Renamed “Environments” to “Environments & API Keys” 2023-06-16 16:30:35 +01:00
Matt Aitken 081ef45047 Deleted org environment page, they are at the Project level 2023-06-16 16:30:35 +01:00
James Ritchie 9aa9ebc23f Minor tweak to the button min width 2023-06-16 16:26:58 +01:00
James Ritchie f664a10024 Final style improvements for the clipboard component 2023-06-16 16:22:33 +01:00
James Ritchie 52beb2f012 Removed borders on the icon 2023-06-16 16:14:03 +01:00
James Ritchie 149e3c2d84 Style updates and you can now click the icon to focus the field 2023-06-16 16:06:50 +01:00
James Ritchie 223965757c Making the clipboard component work with icons and larger sizes 2023-06-16 15:04:03 +01:00
James Ritchie 27c1d2516b More variants to the clipboard field 2023-06-16 15:04:03 +01:00
James Ritchie 014a97fcdc improved the env labels 2023-06-16 15:04:03 +01:00
James Ritchie eb85cc5c14 Adding new variants to the clipboard component 2023-06-16 15:04:03 +01:00
James Ritchie b1d49d71ea Removed import 2023-06-16 15:04:03 +01:00
James Ritchie 9a0a6305e7 Added a clipboard field story 2023-06-16 15:04:03 +01:00
James Ritchie a218bce73b Added prisma migrate deploy to package 2023-06-16 15:04:03 +01:00
Eric Allam c94ca54aff Add chat completion task 2023-06-16 15:02:24 +01:00
Eric Allam 2e8cd805fd Fixed for the new integrations stuff 2023-06-16 14:49:52 +01:00
Eric Allam 1a9bb4ddc5 Fix for seeds 2023-06-16 14:49:37 +01:00
Eric Allam e7f7c0704a Revamped the integration schema and local only integrations now work in the UI 2023-06-16 14:15:17 +01:00
Matt Aitken b5e0fb0e97 Tightened up the API grid 2023-06-16 11:53:13 +01:00
Matt Aitken f555989a5c Added a full stop 2023-06-16 11:53:03 +01:00
Matt Aitken 2e0ea4520c Updated to the latest @trigger.dev/companyicons package 2023-06-16 11:41:38 +01:00
Matt Aitken 31a9164df1 Added API integration voting functionality 2023-06-16 11:32:38 +01:00
Matt Aitken b363a1e60d Remove coming soon message from the OAuth form 2023-06-16 11:32:26 +01:00
Matt Aitken e5714f9047 Call them APIs in the list 2023-06-16 11:32:13 +01:00
Matt Aitken e9b9c77edc Added a NoIntegrationSheet 2023-06-16 10:10:06 +01:00
Matt Aitken acd49994bd WIP on showing APIs we don’t yet have an integration for 2023-06-15 17:38:56 +01:00
Matt Aitken 5264674cda Improved the spacing of the LabelValueStack 2023-06-15 17:22:49 +01:00
Matt Aitken 2825925781 Removed a ton of unused imports 2023-06-15 17:22:38 +01:00
Matt Aitken 083bd7f1cf Limit the height of the run output in the left-hand column 2023-06-15 17:03:57 +01:00
Matt Aitken 3f90fe04b4 The OAuth form is back and a coming soon for end-user OAuth 2023-06-15 17:03:57 +01:00
Matt Aitken a63a247b22 The flow is now roughly setup 2023-06-15 17:03:57 +01:00
Matt Aitken 7bb589b8f7 Multiple samples for API Keys 2023-06-15 17:03:57 +01:00
Matt Aitken 7c94ea3ec5 Added CustomHelp 2023-06-15 17:03:57 +01:00
Matt Aitken 5789ae3c73 Sheet scrolling should now work properly… painful 2023-06-15 17:03:57 +01:00
Matt Aitken 95fad3deb8 API auth help 2023-06-15 17:03:57 +01:00
Matt Aitken 2b5b8d7931 Added Paragraph spacing 2023-06-15 17:03:57 +01:00
Matt Aitken 96e0fb81ae Updated packages 2023-06-15 17:03:57 +01:00
Matt Aitken e5a568f510 Improved the radio button styling 2023-06-15 17:03:57 +01:00
Matt Aitken 8cb3155ea6 WIP on new integration flow 2023-06-15 17:03:57 +01:00
Matt Aitken 52c5d93449 Added a RadioGroup component with the same variants as Checkbox 2023-06-15 17:03:57 +01:00
Matt Aitken 5e3502f589 Removed badge prop from Checkbox (it had badges already) 2023-06-15 17:03:56 +01:00
Matt Aitken f1fd36d3a1 Checkbox description style doesn’t use mono font 2023-06-15 17:03:56 +01:00
Eric Allam 9f49b7f4c0 Move prisma to an internal database package 2023-06-15 14:18:34 +01:00
Eric Allam eca89e538b Convert to useTypedRouteLoaderData and improve the hydrateDates function 2023-06-14 21:53:04 +01:00
Eric Allam bbac63e162 Use /trigger to select the trigger details pane, and no longer redirecting when you land on the run page (can just link directly to /trigger if we want to) 2023-06-14 21:42:39 +01:00
Eric Allam ed3123470a Replaced manual hydration with using remix-typedjson (fixes error on run page) 2023-06-14 18:04:28 +01:00
Eric Allam 170bda3158 Only show the retry job button when status is failed 2023-06-14 17:09:19 +01:00
Eric Allam b5a8eec650 Implement the continue run service 2023-06-14 17:03:15 +01:00
Eric Allam 817b4ed133 Endpoint registration and indexing now is only initiated outside of clients 2023-06-14 16:07:10 +01:00
Matt Aitken e4837cea9d Run left-hand scrollbar now positioned better 2023-06-14 15:48:06 +01:00
Matt Aitken e1cdeb284f Created the ContinueRunService for Eric to implement 2023-06-14 14:18:28 +01:00
Matt Aitken f195933cb6 Rerunning from scratch is working 2023-06-14 14:16:44 +01:00
Matt Aitken 6e1d91b413 Moved the ReRunPopover to a component to make the page easier to work with 2023-06-14 12:47:52 +01:00
Matt Aitken d4e39de67e WIP on Rerun/continue functionality 2023-06-14 12:46:17 +01:00
Matt Aitken 4903cfc689 Don’t let Callout icons shrink when the space is small 2023-06-14 12:45:56 +01:00
Matt Aitken 54a8e26c7c Organized imports 2023-06-14 12:02:17 +01:00
Matt Aitken 4cc9006c8e Added test help panel. Defaults open if there haven’t been any test runs yet 2023-06-14 12:00:43 +01:00
Matt Aitken bc6492b1c4 Improved the test layout so the examples are more obvious 2023-06-14 11:52:11 +01:00
Matt Aitken afd05f93fc Tried to improve the environment label but it’s a bit fooked 2023-06-14 11:52:11 +01:00
Matt Aitken e83f5e7cca Fix for DynamicTriggerIcon non camelCase attributes 2023-06-14 11:52:11 +01:00
Matt Aitken 7b8e02023e Default icon for an example is the beaker 2023-06-14 11:52:11 +01:00
Matt Aitken 3882f531cf Environment is now in the rounded rectangle so it’s consistent 2023-06-14 11:52:11 +01:00
Matt Aitken 0ce58b5c0f Better spacing for PageHeader buttons 2023-06-14 11:52:11 +01:00
Eric Allam f36a1c60f4 Changeset: version nextjs 2023-06-14 09:55:02 +01:00
Eric Allam 98fcc5d2d4 Make sure nextjs app routes have the correct types 2023-06-14 09:55:02 +01:00
Matt Aitken 6ff2ee23f8 Hide the test button if you’re on the /test page 2023-06-14 09:15:19 +01:00
Matt Aitken e1405b140f Better test layout 2023-06-14 09:13:15 +01:00
Matt Aitken 04ab709ddd Better test styling 2023-06-14 09:09:43 +01:00
Eric Allam 0736c68d77 Fixed duplicate export issue 2023-06-14 08:08:52 +01:00
Eric Allam 34ccf345cd Changeset version: implement task errors and retrying 2023-06-14 08:07:19 +01:00
Matt Aitken 605a38d8ab Trying to make a good layout for the test page 2023-06-13 18:16:07 +01:00
Matt Aitken c9fd8ccae2 Running tests is working 2023-06-13 16:33:42 +01:00
Matt Aitken 43482b1b96 Get some errors if you’ve used invalid JSON 2023-06-13 15:52:58 +01:00
Matt Aitken 81912b2530 Test page is working with a ref 2023-06-13 15:52:58 +01:00
Matt Aitken 2db2e7a369 First version of the Test page with example and environment selection 2023-06-13 15:52:58 +01:00
Eric Allam 3589d26fb3 Implement task retrying to recording task attempts 2023-06-13 15:48:07 +01:00
Matt Aitken e5282fb264 Get the example data for the test page 2023-06-13 11:48:39 +01:00
Matt Aitken 61fddbb8af Added name and icon to the EventExamples 2023-06-13 11:48:27 +01:00
Matt Aitken e141aa3ff4 Added an GitHub Issue opened example 2023-06-13 10:57:06 +01:00
Matt Aitken 2fa1536315 EventExamples are now in a separate table (upserted on job register) 2023-06-13 10:56:57 +01:00
Matt Aitken 5607dcfae9 Created useFilterJobs which also searches the job slug 2023-06-13 10:11:34 +01:00
Eric Allam bd103802f9 Better logging of errors so we can debug delivery issues to hosts 2023-06-12 21:11:44 +01:00
Matt Aitken 0cbf427a58 Added “Test” to the breadcrumb 2023-06-12 18:18:23 +01:00
Matt Aitken e53cd477f9 Added a dynamic property to the Job page header 2023-06-12 18:15:45 +01:00
Matt Aitken df61f29a02 Added Dynamic markers to the jobs table 2023-06-12 18:12:52 +01:00
Matt Aitken feda01ab3b Don’t count internal or other project jobs in the integration table 2023-06-12 16:28:42 +01:00
Matt Aitken fb2684c0b3 Added the placeholder for help using an integration 2023-06-12 16:19:52 +01:00
Matt Aitken 89e4b80e65 Added integration breadcrumbs 2023-06-12 16:12:07 +01:00
Matt Aitken d55fe2d100 Added scopes page 2023-06-12 15:59:34 +01:00
Matt Aitken 49a510aaf9 Client Connections page is working 2023-06-12 15:34:51 +01:00
Matt Aitken 16a5504a32 Connection client jobs page 2023-06-12 14:47:47 +01:00
Matt Aitken b720c27c2a Show the slug in a ClipboardField 2023-06-12 14:12:51 +01:00
Matt Aitken cad3453e32 Created the ClipboardField 2023-06-12 14:05:38 +01:00
Matt Aitken afe0a8a159 Run page streaming runs just once if run is completed 2023-06-12 12:04:36 +01:00
Matt Aitken 2487e815a3 Use zod for integration client page params parsing 2023-06-12 12:04:36 +01:00
Eric Allam 771348dd5e Handle task errors and update the task status and add errors to the task output 2023-06-12 09:31:47 +01:00
Matt Aitken 1f76e1228a Added connection client Jobs table 2023-06-12 09:26:15 +01:00
Eric Allam fe782e5ff8 Scope finding projects and runs by the project and org 2023-06-09 21:14:40 +01:00
Eric Allam b4167a38f2 Fixed the event trigger name 2023-06-09 21:14:15 +01:00
Matt Aitken 61459a197f Started working on the Integration client page 2023-06-09 19:23:53 +01:00
Matt Aitken 30b6cc646a Improved the integrations page layout 2023-06-09 17:53:02 +01:00
Matt Aitken df95d4e29f Changed to using the job slug in the urls 2023-06-09 17:26:52 +01:00
Eric Allam 5090b64e57 Store the graphile job ID on the executions to be able to fast-forward delayed tasks 2023-06-09 16:56:45 +01:00
Matt Aitken 12d188d85a When accepting an invite it creates the RuntimeEnvironment for each project 2023-06-09 16:51:26 +01:00
Matt Aitken ab3e45d933 Import tidying 2023-06-09 16:40:47 +01:00
Matt Aitken 8c979cabd7 Added the realtime duration to the detail panel 2023-06-09 16:39:15 +01:00
Matt Aitken a617922c1d Handles closing out the realtime connection better 2023-06-09 16:37:18 +01:00
Matt Aitken fec7cd65a7 The Run page now live updates 2023-06-09 16:21:36 +01:00
Matt Aitken 10cfdb2fa2 Tweaked the countdown 2023-06-09 16:21:36 +01:00
Eric Allam 43a62d8bec Always create environments when creating a project 2023-06-09 15:40:18 +01:00
Matt Aitken 7b32b7ecd6 Delays display well now with countdowns 2023-06-09 15:25:34 +01:00
Eric Allam 2fcd166d32 Upgrade nextjs package to not require a specific next version 2023-06-09 14:42:50 +01:00
Eric Allam d8c673157b Changset: versions 2023-06-09 14:42:50 +01:00
Eric Allam 99316df879 Adding changeset for v2 2023-06-09 14:42:50 +01:00
Matt Aitken 5a4fa3d2b3 Return data from schedule job 2 2023-06-09 12:11:38 +01:00
Matt Aitken 71ececd7ce Added completed detail panel, improved panels 2023-06-09 12:09:28 +01:00
Matt Aitken 74519c08f0 Task errors are now parsed and displayed using the error component 2023-06-09 11:42:15 +01:00
Matt Aitken a85f19e016 The schedule job throws a task error 2023-06-09 11:41:58 +01:00
Matt Aitken cfe887341d Latest lockfile from running pnpm i 2023-06-09 11:22:07 +01:00
Matt Aitken eab17c3467 Added a state when there are no tasks 2023-06-09 11:20:15 +01:00
Matt Aitken 87c598627f Added a TaskCardSkeleton, it needs making good 2023-06-09 11:20:15 +01:00
Matt Aitken 30dc840144 Export the RunBasicStatus 2023-06-09 11:20:15 +01:00
Matt Aitken dd0e2d0bd2 Improved the run output 2023-06-09 11:20:15 +01:00
690 changed files with 19912 additions and 7673 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Adding support for trigger source in the run context, and make sure dynamic trigger runs are preprocessed so they have a chance of populating run properties
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/github": patch
---
Fixed the broken export issue with two of the new tasks
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/init": patch
---
Fix for Next.js projects using the src dir
+2 -6
View File
@@ -8,11 +8,7 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"wss",
"webapp",
"integrations",
"emails",
"internal-cli",
"integration-catalog"
"emails"
]
}
}
+11
View File
@@ -0,0 +1,11 @@
---
"@trigger.dev/integration-kit": patch
"@trigger.dev/sdk": patch
"@trigger.dev/github": patch
"@trigger.dev/openai": patch
"@trigger.dev/slack": patch
"@trigger.dev/nextjs": patch
"@trigger.dev/init": patch
---
Creating the init CLI package
+8
View File
@@ -0,0 +1,8 @@
---
"@trigger.dev/sdk": major
"@trigger.dev/github": major
"@trigger.dev/slack": major
"@trigger.dev/nextjs": major
---
Preparing packages for V2
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/init": patch
---
Fixed the version of the @trigger.dev/nextjs package
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Endpoint registration and indexing now is only initiated outside of clients
+7
View File
@@ -0,0 +1,7 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/github": patch
"@trigger.dev/nextjs": patch
---
Add support for task errors and task retrying
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/init": patch
---
Improved the CLI prompts
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Fixed the eventTrigger name
+8
View File
@@ -0,0 +1,8 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/github": patch
"@trigger.dev/openai": patch
"@trigger.dev/slack": patch
---
Added support for backgroundFetch
+39
View File
@@ -0,0 +1,39 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"webapp": "1.0.0",
"@trigger.dev/github": "0.2.0",
"@trigger.dev/slack": "0.2.0",
"create-trigger": "0.2.0",
"emails": "1.0.0",
"@trigger.dev/internal": "0.0.1",
"@trigger.dev/nextjs": "0.1.0",
"@trigger.dev/sdk": "1.0.0",
"@trigger.dev/openai": "1.0.0-next.0",
"@trigger.dev/database": "0.0.0",
"@trigger.dev/init": "0.2.0",
"@trigger.dev/integration-kit": "2.0.0-next.1",
"@trigger.dev/resend": "1.0.0-next.1"
},
"changesets": [
"bright-horses-bathe",
"chilly-meals-warn",
"clever-bats-learn",
"curly-hornets-sort",
"dirty-needles-kneel",
"eighty-queens-prove",
"four-ligers-perform",
"great-meals-listen",
"heavy-mails-explain",
"itchy-rockets-provide",
"modern-emus-suffer",
"quick-stingrays-own",
"strong-cars-visit",
"stupid-files-cough",
"sweet-snails-refuse",
"tame-flowers-invite",
"tough-walls-share",
"wild-rats-mix"
]
}
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/init": patch
---
Fixed the help defaults
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/init": patch
---
Fixed an issue that cause environment variables to be incorrectly added to .env.local
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/nextjs": patch
---
Loosen the peer dep requirement on next
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/init": patch
---
Fixed CLI showing undefined when not specifying a trigger-url flag
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/resend": patch
---
Adding the resend integration package
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/nextjs": patch
---
Make sure nextjs app routes have the correct types
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/nextjs": patch
---
Update the devDependency on nextjs to be more permissive
-8
View File
@@ -1,8 +0,0 @@
# You need to stop and re-run docker to switch, `pnpm run docker:db`
# To login using GitHub and connect via OAuth with most integrations
AUTH_CALLBACK_URL=http://localhost:3004/oauth/callback
# To connect with Slack
# AUTH_CALLBACK_URL=https://<ngrok subdomain>.eu.ngrok.io/oauth/callback
# You only need to use this locally if you specify PIZZLY_SECRET_KEY in the webapp .env file
# PIZZLY_SECRET_KEY=<secret_key>
+36
View File
@@ -0,0 +1,36 @@
# YOU MUST SET THESE VARIABLES
# Resend is an email service used for signing in to Trigger.dev via a Magic Link.
### Visit https://resend.com, create an account and get your API key. Then insert it below along with your From and Reply To email addresses. Visit https://resend.com/docs for more information.
RESEND_API_KEY=<api_key>
FROM_EMAIL=
REPLY_TO_EMAIL=
# YOU MIGHT LIKE TO MODIFY THESE VARIABLES
SESSION_SECRET=abcdef1234
MAGIC_LINK_SECRET=abcdef1234
LOGIN_ORIGIN=http://localhost:3030
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public
REMIX_APP_PORT=3030
APP_ENV=development
APP_ORIGIN=http://localhost:3030
NODE_ENV=development
# OPTIONAL VARIABLES
AUTH_GITHUB_CLIENT_ID=
AUTH_GITHUB_CLIENT_SECRET=
# CLOUD VARIABLES
POSTHOG_PROJECT_KEY=
PLAIN_API_KEY=
SEED_CLOUD=
SEED_CLOUD_EMAIL=
SEED_CLOUD_GITHUB_ACCESS_TOKEN=
SEED_CLOUD_SLACK_ACCESS_TOKEN=
SEED_CLOUD_USER_GITHUB_ACCESS_TOKEN=
CLOUD_AIRTABLE_CLIENT_ID=
CLOUD_AIRTABLE_CLIENT_SECRET=
CLOUD_GITHUB_CLIENT_ID=
CLOUD_GITHUB_CLIENT_SECRET=
CLOUD_SLACK_APP_HOST=
CLOUD_SLACK_CLIENT_ID=
CLOUD_SLACK_CLIENT_SECRET=
+153
View File
@@ -0,0 +1,153 @@
name: 🚀 Publish Trigger.dev Docker
on:
push:
branches:
- main
tags:
- "v*"
permissions:
id-token: write
packages: write
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
env:
AWS_REGION: us-east-1
jobs:
typecheck:
name: ʦ TypeScript
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7.18
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: 📀 Generate Prisma Client
run: pnpm run generate
- name: 🔎 Type check
run: pnpm run typecheck --filter webapp
publish:
needs: [typecheck]
runs-on: buildjet-4vcpu-ubuntu-2204
outputs:
version: ${{ steps.get_version.outputs.version }}
short_sha: ${{ steps.get_commit.outputs.sha_short }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: 🆚 Get the version
id: get_version
run: |
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "IMAGE_TAG=${IMAGE_TAG}"
elif [[ $GITHUB_REF == refs/heads/* ]]; then
IMAGE_TAG="${GITHUB_REF#refs/heads/}"
echo "IMAGE_TAG=${IMAGE_TAG}"
else
echo "Invalid reference: ${GITHUB_REF}"
exit 1
fi
echo ::set-output name=version::${IMAGE_TAG}
- name: 🔢 Get the commit hash
id: get_commit
run: |
echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7)
- name: 🐳 Build Docker Image
run: |
docker build -t release_build_image -f ./docker/Dockerfile .
- name: 🐙 Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🐙 Push to GitHub Container Registry
run: |
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
env:
REGISTRY: ghcr.io/triggerdotdev
REPOSITORY: trigger.dev
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
- name: 🐙 Push 'latest' to GitHub Container Registry
if: startsWith(github.ref, 'refs/tags/')
run: |
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
docker push $REGISTRY/$REPOSITORY:latest
env:
REGISTRY: ghcr.io/triggerdotdev
REPOSITORY: trigger.dev
- name: 🌐 Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::067435167850:role/GitHubAction-AssumeRoleWithAction
aws-region: ${{ env.AWS_REGION }}
- name: 🌐 Login to ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v1
- name: 🌐 Push to Amazon ECR
run: |
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
env:
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
REPOSITORY: trigger.dev
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
- name: 🌐 Push 'latest' to Amazon ECR
if: startsWith(github.ref, 'refs/tags/')
run: |
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
docker push $REGISTRY/$REPOSITORY:latest
env:
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
REPOSITORY: trigger.dev
- name: 🌐 Push commit hash to Amazon ECR
if: startsWith(github.ref, 'refs/heads/')
run: |
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
env:
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
REPOSITORY: trigger.dev
IMAGE_TAG: ${{ steps.get_version.outputs.version }}-${{ steps.get_commit.outputs.sha_short }}
- name: 🚀 Trigger Test Deploy
if: github.ref == 'refs/heads/main' && github.event_name == 'push' && github.repository == 'triggerdotdev/trigger.dev'
env:
DEPLOY_TEST_HOOK: ${{ secrets.DEPLOY_TEST_HOOK }}
run: |
curl -X POST $DEPLOY_TEST_HOOK
+67
View File
@@ -0,0 +1,67 @@
name: 🦋 Changesets Release
on:
push:
branches:
- main
jobs:
release:
name: 🦋 Changesets Release
runs-on: buildjet-4vcpu-ubuntu-2204
if: |
github.repository == 'triggerdotdev/trigger.dev'
outputs:
published_packages: ${{ steps.changesets.outputs.publishedPackages }}
published: ${{ steps.changesets.outputs.published }}
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7.18
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: 📀 Generate Prisma Client
run: pnpm run generate
- name: 🔎 Type check
run: pnpm run typecheck
- name: 🔐 Setup npm auth
run: |
echo "registry=https://registry.npmjs.org" >> ~/.npmrc
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc
# This action has two responsibilities. The first time the workflow runs
# (initial push to the `main` branch) it will create a new branch and
# then open a PR with the related changes for the new version. After the
# PR is merged, the workflow will run again and this action will build +
# publish to npm.
- name: 🚀 PR / Publish
id: changesets
uses: changesets/action@v1
with:
version: pnpm run changeset:version
commit: "chore: Update version for release"
title: "chore: Update version for release"
publish: pnpm run changeset:release
createGithubReleases: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+3 -1
View File
@@ -46,4 +46,6 @@ yarn-error.log*
apps/**/public/build
.tests-container-id.txt
.sentryclirc
.buildt
.buildt
**/tmp/
+2 -1
View File
@@ -1 +1,2 @@
link-workspace-packages=false
link-workspace-packages=false
public-hoist-pattern[]=*prisma*
-73
View File
@@ -1,73 +0,0 @@
# Warp Launch Configuration
#
#
# Use this to start a certain configuration of windows, tabs, and panes
# Open the launch configuration palette to access and open any launch configuration
#
# This file defines your launch configuration
# More on how to do so here:
# https://docs.warp.dev/features/session-management/launch-configurations
#
# All launch configurations are stored under ~/.warp/launch_configurations/
# Edit them anytime!
#
# You can also add commands that run on-start for your launch configurations like so:
# ---
# name: Example with Command
# windows:
# - tabs:
# - layout:
# cwd: /Users/warp-user/project
# commands:
# - exec: code .
---
name: Triggerdotdev
windows:
- tabs:
- title: webapp
layout:
split_direction: vertical
panes:
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
commands:
- exec: pnpm run build --filter webapp
- exec: pnpm run i-dev --filter webapp
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/webapp
- title: integrations
layout:
split_direction: vertical
panes:
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
commands:
- exec: pnpm run dev --filter integrations
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/integrations
- title: ngrok
layout:
split_direction: horizontal
panes:
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
commands:
- exec: ./scripts/proxy-webapp.sh <your-trigger-dev>
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
commands:
- exec: ./scripts/proxy-pizzly.sh <your-pizzly-dev>
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
commands:
- exec: ./scripts/proxy-wss.sh <your-wss-dev>
- title: wss and examples
layout:
split_direction: horizontal
panes:
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/wss
commands:
- exec: LOG_LEVEL=debug pnpm run dev
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/examples
- title: emails
layout:
split_direction: horizontal
panes:
- cwd: /Users/eric/Work/Git/APIHero/trigger.dev/packages/emails
commands:
- exec: pnpm run dev
- cwd: /Users/eric/Work/Git/APIHero/trigger.dev/packages/emails
+9 -10
View File
@@ -4,23 +4,22 @@ Trigger.dev uses [changesets](https://github.com/changesets/changesets) to manag
## Adding a changeset
To add a changeset, use `pnpm run changeset:add` and follow the instructions [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please only ever select one of our public packages when adding a changeset, which currently are:
To add a changeset, use `pnpm run changeset:add` and follow the instructions [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please only ever select one of our public packages when adding a changeset.
- `@trigger.dev/sdk`
- `@trigger.dev/integration-sdk`
- `@trigger.dev/github`
- `@trigger.dev/notion`
- `@trigger.dev/slack`
- `@trigger.dev/shopify`
- `@trigger.dev/resend`
## Release instructions
## Release instructions (local only)
Based on the instructions [here](https://github.com/changesets/changesets/blob/main/docs/intro-to-using-changesets.md)
1. Run `pnpm run changeset:version`
2. Run `pnpm run changeset:release`
## Release instructions (CI)
Please follow the best-practice of adding changesets in the same commit as the code making the change with `pnpm run changeset:add`, as it will allow our release.yml CI workflow to function properly:
- Anytime new changesets are added in a commit in the `main` branch, the [release.yml](./.github/workflows/release.yml) workflow will run and will automatically create/update a PR with a fresh run of `pnpm run changeset:version`.
- When the version PR is merged into `main`, the release.yml workflow will automatically run `pnpm run changeset:release` to build and release packages to npm.
## Pre-release instructions
1. Add changesets as usual `pnpm run changeset:add`
+196 -20
View File
@@ -1,25 +1,201 @@
Copyright (c) 2022 API Hero Inc.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Portions of this software are licensed as follows:
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
- All content that resides under any "ee/" directory of this repository, if such directories exists, are licensed under the license defined in "ee/LICENSE".
- All third party components incorporated into the Trigger.dev Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
1. Definitions.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+90 -6
View File
@@ -4,18 +4,102 @@
<source media="(prefers-color-scheme: light)" srcset="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/3f5ad4c1-c4c8-4277-b622-290e7f37bd00/public">
<img alt="Trigger.dev logo" src="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/a45d1fa2-0ae8-4a39-4409-f4f934bfae00/public">
</picture>
### The background jobs framework for Next.js
[Discord](https://discord.gg/bpVDSjcG) | [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 background jobs framework for Next.js**
# About Trigger.dev
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
&nbsp;
### Long running Jobs on serverless
### _**Trigger.dev v2 is launching soon...**_
Reliably run jobs and dont worry about function timeouts, we handle those for you.
Visit the new landing page for [Trigger.dev v2](https://next.trigger.dev) and [sign up for early access.](https://bcymafitv0e.typeform.com/to/QQnotGJM#source=github)
- Auto-resume after a function timeout
- Auto-resume after a server outage
- Add delays of up to a year
### In your codebase
Create Jobs where they belong: in your codebase. Version control, localhost, test, review, and deploy like you're already used to.
### Secure by design
We only receive Triggers and the data you choose to send to us. You can even completely self-host the entire platform.
### Don't worry about deployment
Just use our SDK to write Jobs in your Next.js 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.
### Full visibility of every job run
View every Task in every Run so you can tell exactly what happened.
![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.
### Trigger.dev Connect (coming soon)
Easily add integrations for your users.
# Getting started
Visit our docs [here](https://trigger.dev/docs).
## 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.
## Development
### Prerequisites
- [Node.js](https://nodejs.org/en) version >=18.x
- [pnpm package manager](https://pnpm.io/installation) version 7
- [Docker](https://www.docker.com/get-started/)
### Setup
1. Clone the repo into a public GitHub repository or [fork the repo](https://github.com/triggerdotdev/trigger.dev/fork). If you plan to distribute the code, keep the source code public to comply with the [Apache Licence 2.0](https://github.com/triggerdotdev/trigger.dev/blob/main/LICENSE).
```
git clone https://github.com/triggerdotdev/trigger.dev.git
```
> If you are on windows, run the following command on gitbash with admin privileges:
> `git clone -c core.symlinks=true https://triggerdotdev/trigger.dev.git`
2. Navigate to the project folder
```
cd trigger.dev
```
3. Install the required packages using pnpm.
```
pnpm i
```
4. Create your `.env` files
```
cp .env.example .env && cp packages/database/.env.example packages/database/.env
```
Open the root `.env` file and fill in the required values.
6. Start Docker. This starts the required services like Postgres.
```
pnpm run docker
```
7. Migrate the database
```
pnpm run db:migrate
```
8. Run the seed script
```
pnpm run db:seed
```
5. Run the project. It should run on `port:3030`
```
pnpm run dev --filter webapp
```
-72
View File
@@ -1,72 +0,0 @@
TRIGGER_API_KEY=
TRIGGER_LOG_LEVEL=debug
##########################################################################################
# ************************************* BASICS *******************************************
##########################################################################################
APP_ENV=development
APP_ORIGIN=http://localhost:3000
NODE_ENV=development
PULSAR_ENABLED=1
SESSION_SECRET=
#########################################################################################
# ************************************* LOGIN *******************************************
#########################################################################################
GITHUB_CLIENT_ID=
GITHUB_SECRET=
LOGIN_ORIGIN=http://localhost:3000
MAGIC_LINK_SECRET=
############################################################################################
# ************************************ OPTIONAL ********************************************
############################################################################################
POSTHOG_PROJECT_KEY=
############################################################################################
# ************************************ DATABASE ********************************************
############################################################################################
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
################################################################################################
# ********************************** INTEGRATIONS **********************************************
################################################################################################
PIZZLY_HOST=http://localhost:3004
PIZZLY_SECRET_KEY=
#########################################################################################
# ************************************* EMAIL *******************************************
#########################################################################################
FROM_EMAIL=
REPLY_TO_EMAIL=
RESEND_API_KEY=<api_key>
+8 -3
View File
@@ -3,6 +3,7 @@ import "../app/tailwind.css";
import { unstable_createRemixStub } from "@remix-run/testing";
import React from "react";
import { LocaleContextProvider } from "../app/components/primitives/LocaleProvider";
import { OperatingSystemContextProvider } from "../app/components/primitives/OperatingSystemProvider";
const preview: Preview = {
parameters: {
@@ -33,9 +34,13 @@ const preview: Preview = {
]);
return (
<LocaleContextProvider locales={window.navigator.languages as string[]}>
<RemixStub initialEntries={["/"]} />
</LocaleContextProvider>
<OperatingSystemContextProvider platform="mac">
<LocaleContextProvider
locales={window.navigator.languages as string[]}
>
<RemixStub initialEntries={["/"]} />
</LocaleContextProvider>
</OperatingSystemContextProvider>
);
},
],
-74
View File
@@ -1,74 +0,0 @@
FROM node:16-bullseye AS pruner
RUN apt-get update && apt-get install openssl -y
WORKDIR /app
RUN npm install turbo@1.7.0 -g
COPY . .
RUN turbo prune --scope=webapp --docker
RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
# Base strategy to have layer caching
FROM node:16-bullseye AS base
RUN apt-get update && apt-get install openssl ca-certificates g++ make wget python3 -y
ENV PULSAR_CPP_CLIENT_VERSION=2.10.3
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client.deb -q
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client-dev.deb -q
RUN dpkg -i ./apache-pulsar-client*.deb
WORKDIR /app
COPY .gitignore .gitignore
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
FROM base AS production-deps
WORKDIR /app
RUN npm install turbo@1.7.0 -g
RUN corepack enable
ENV NODE_ENV production
RUN npm config set python /usr/bin/python3
COPY --from=pruner /app/out/full/apps/webapp/prisma/schema.prisma /app/apps/webapp/prisma/schema.prisma
RUN pnpm install --prod --frozen-lockfile
RUN pnpx prisma generate --schema /app/apps/webapp/prisma/schema.prisma
FROM base AS builder
WORKDIR /app
RUN npm install turbo@1.7.0 -g
COPY turbo.json turbo.json
RUN corepack enable
COPY --from=pruner /app/out/full/ .
RUN npm config set python /usr/bin/python3
ENV NODE_ENV development
RUN pnpm install --ignore-scripts --frozen-lockfile
ENV NODE_ENV production
RUN pnpm run generate
RUN pnpm run build --filter=webapp...
RUN pnpx prisma migrate deploy --schema apps/webapp/prisma/schema.prisma
RUN pnpm run db:seed --filter=webapp
# Runner
FROM node:16-bullseye AS runner
RUN apt-get update && apt-get install openssl ca-certificates g++ make wget python3 -y
ENV PULSAR_CPP_CLIENT_VERSION=2.10.3
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client.deb -q
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client-dev.deb -q
RUN dpkg -i ./apache-pulsar-client*.deb
RUN npm install turbo -g
WORKDIR /app
RUN corepack enable
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 remixjs
RUN chown -R remixjs:nodejs /app
USER remixjs
COPY --from=pruner --chown=remixjs:nodejs /app/out/full/ .
COPY --from=production-deps --chown=remixjs:nodejs /app .
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/app/styles/tailwind.css ./apps/webapp/app/styles/tailwind.css
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/build/server.js ./apps/webapp/build/server.js
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/build ./apps/webapp/build
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/public ./apps/webapp/public
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/prisma/schema.prisma ./apps/webapp/build/schema.prisma
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/prisma/migrations ./apps/webapp/build/migrations
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/node_modules/.prisma/client/libquery_engine-debian-openssl-1.1.x.so.node ./apps/webapp/build/libquery_engine-debian-openssl-1.1.x.so.node
# release_command = "pnpx prisma migrate deploy --schema apps/webapp/prisma/schema.prisma"
ENTRYPOINT ["pnpm", "--filter", "webapp", "run", "start"]
@@ -0,0 +1,17 @@
export function DynamicTriggerIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 21 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.0151 4C2.01629 2.89543 2.91269 2.00097 4.01726 2.00216L16.0172 2.0151C17.1218 2.01629 18.0163 2.91269 18.0151 4.01726L18.0021 16.0172C18.001 17.1218 17.1046 18.0163 16 18.0151L4 18.0021C2.89543 18.001 2.00097 17.1046 2.00216 16L2.0151 4ZM9.26429 4.75782C9.26473 4.34361 9.60088 4.00818 10.0151 4.00863C10.4293 4.00907 10.7647 4.34522 10.7643 4.75944L10.7606 8.1988L13.1952 5.76942C13.4884 5.47684 13.9633 5.47735 14.2559 5.77056C14.5484 6.06377 14.5479 6.53864 14.2547 6.83122L11.8201 9.26058L15.2594 9.26429C15.6736 9.26473 16.0091 9.60088 16.0086 10.0151C16.0082 10.4293 15.672 10.7647 15.2578 10.7643L11.8185 10.7606L14.2478 13.1952C14.5404 13.4884 14.5399 13.9633 14.2467 14.2558C13.9535 14.5484 13.4786 14.5479 13.186 14.2547L10.7567 11.8201L10.753 15.2594C10.7525 15.6736 10.4164 16.0091 10.0022 16.0086C9.58794 16.0082 9.25252 15.672 9.25296 15.2578L9.25667 11.8185L6.82209 14.2478C6.52888 14.5404 6.05401 14.5399 5.76143 14.2467C5.46885 13.9535 5.46937 13.4786 5.76258 13.186L8.19718 10.7567L4.75782 10.753C4.34361 10.7525 4.00818 10.4164 4.00863 10.0022C4.00907 9.58794 4.34522 9.25252 4.75944 9.25296L8.19877 9.25667L5.76942 6.82207C5.47684 6.52886 5.47735 6.05399 5.77056 5.76141C6.06377 5.46883 6.53864 5.46935 6.83122 5.76255L9.26058 8.19716L9.26429 4.75782Z"
fill="currentColor"
/>
</svg>
);
}
+37
View File
@@ -0,0 +1,37 @@
import { ReactNode } from "react";
import { MainCenteredContainer } from "./layout/AppLayout";
import { Header1, Header2 } from "./primitives/Headers";
import { NamedIcon, NamedIconInBox } from "./primitives/NamedIcon";
import { Paragraph } from "./primitives/Paragraph";
type ComingSoonProps = {
title: string;
description: string;
icon: ReactNode;
};
export function ComingSoon({ title, description, icon }: ComingSoonProps) {
return (
<MainCenteredContainer>
<div className="flex flex-col justify-center rounded border border-slate-800 bg-slate-850">
<div className="flex items-center gap-2 border-b border-slate-750 px-4 py-4">
{typeof icon === "string" ? (
<NamedIconInBox
name={icon}
className={"h-10 w-10 bg-midnight-800"}
/>
) : (
icon
)}
<div className="mt-0.5 flex flex-col">
<Paragraph variant="extra-extra-small/caps">Coming soon</Paragraph>
<Header1 className="-mt-0.5">{title}</Header1>
</div>
</div>
<Paragraph variant="small" className="p-4">
{description}
</Paragraph>
</div>
</MainCenteredContainer>
);
}
+122
View File
@@ -0,0 +1,122 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/solid";
import {
Form,
useActionData,
useLocation,
useNavigation,
} from "@remix-run/react";
import { useState } from "react";
import { feedbackTypeLabel, schema } from "~/routes/resources.feedback";
import { Button } from "./primitives/Buttons";
import { Fieldset } from "./primitives/Fieldset";
import { FormButtons } from "./primitives/FormButtons";
import { FormError } from "./primitives/FormError";
import { InputGroup } from "./primitives/InputGroup";
import { Label } from "./primitives/Label";
import { Paragraph } from "./primitives/Paragraph";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "./primitives/Select";
import {
Sheet,
SheetBody,
SheetContent,
SheetHeader,
SheetTrigger,
} from "./primitives/Sheet";
import { TextArea } from "./primitives/TextArea";
export function Feedback() {
const [open, setOpen] = useState(false);
const location = useLocation();
const lastSubmission = useActionData();
const navigation = useNavigation();
const [form, { path, feedbackType, message }] = useForm({
id: "accept-invite",
lastSubmission,
onValidate({ formData }) {
return parse(formData, { schema });
},
});
if (
open &&
navigation.formAction === "/resources/feedback" &&
form.error === undefined &&
form.errors.length === 0
) {
setOpen(false);
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild={true}>
<Button
variant="secondary/small"
LeadingIcon={ChatBubbleLeftRightIcon}
shortcut={{ key: "f" }}
>
Send us feedback
</Button>
</SheetTrigger>
<SheetContent size="sm">
<SheetHeader className="justify-between">Give us feedback</SheetHeader>
<SheetBody>
<Paragraph variant="small" className="mb-4">
We'd love to hear your feedback, good, bad or ugly.
</Paragraph>
<Form method="post" action="/resources/feedback" {...form.props}>
<Fieldset>
<input
value={location.pathname}
{...conform.input(path, { type: "hidden" })}
/>
<InputGroup>
<Label>What kind of feedback do you have?</Label>
<SelectGroup>
<Select {...conform.input(feedbackType)} defaultValue={"bug"}>
<SelectTrigger size="medium" width="full">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
{Object.entries(feedbackTypeLabel).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
<FormError id={feedbackType.errorId}>
{feedbackType.error}
</FormError>
</InputGroup>
<InputGroup>
<Label>Message</Label>
<TextArea {...conform.textarea(message)} />
<FormError id={message.errorId}>{message.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<FormButtons
className="max-w-md"
confirmButton={
<Button type="submit" variant="primary/medium">
Send
</Button>
}
/>
</Fieldset>
</Form>
</SheetBody>
</SheetContent>
</Sheet>
);
}
+19 -6
View File
@@ -11,7 +11,7 @@ import { darkTheme } from "./codeMirrorTheme";
import { cn } from "~/utils/cn";
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
content: string;
defaultValue?: string;
language?: "json";
readOnly?: boolean;
onChange?: (value: string) => void;
@@ -33,13 +33,14 @@ const defaultProps: JSONEditorDefaultProps = {
export function JSONEditor(opts: JSONEditorProps) {
const {
content,
defaultValue = "",
language,
readOnly,
onChange,
onUpdate,
onBlur,
basicSetup,
autoFocus,
} = {
...defaultProps,
...opts,
@@ -59,15 +60,15 @@ export function JSONEditor(opts: JSONEditorProps) {
extensions,
editable: !readOnly,
contentEditable: !readOnly,
value: content,
autoFocus: false,
value: defaultValue,
autoFocus,
theme: darkTheme(),
indentWithTab: false,
basicSetup,
onChange,
onUpdate,
};
const { setContainer } = useCodeMirror(settings);
const { setContainer, state } = useCodeMirror(settings);
useEffect(() => {
if (editor.current) {
@@ -75,9 +76,21 @@ export function JSONEditor(opts: JSONEditorProps) {
}
}, [setContainer]);
//if the defaultValue changes update the editor
useEffect(() => {
if (state !== undefined) {
state.update({
changes: { from: 0, to: state.doc.length, insert: defaultValue },
});
}
}, [defaultValue, state]);
return (
<div
className={cn("no-scrollbar overflow-y-auto", opts.className)}
className={cn(
"overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
opts.className
)}
ref={editor}
onBlur={() => {
if (!onBlur) return;
@@ -1,7 +1,7 @@
import { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import { cn } from "~/utils/cn";
type Environment = Pick<RuntimeEnvironment, "type" | "slug">;
type Environment = Pick<RuntimeEnvironment, "type">;
export function EnvironmentLabel({
environment,
@@ -13,7 +13,7 @@ export function EnvironmentLabel({
return (
<span
className={cn(
"rounded-sm px-1 py-0.5 text-xxs font-medium uppercase text-midnight-900",
"flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
environmentColorClassName(environment),
className
)}
@@ -0,0 +1,173 @@
import { Paragraph, TextLink } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
import { Callout } from "../primitives/Callout";
import integrationButton from "./integration-button.png";
import ngrok from "./ngrok.png";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { ClipboardField } from "../primitives/ClipboardField";
import { Button } from "../primitives/Buttons";
import { InlineCode } from "../code/InlineCode";
export function HowToSetupYourProject() {
const devEnvironment = useDevEnvironment();
console.log(devEnvironment);
return (
<>
<StepNumber stepNumber="1" title="Run your Next.js app" />
<StepContentContainer>
<Paragraph>Ensure your Next.js app is running locally.</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run ngrok" />
<StepContentContainer>
<Paragraph spacing>
There are a few ways to do this, but we recommend using{" "}
<TextLink
href="https://trigger.dev/docs/documentation/guides/tunneling-localhost"
target="_blank"
>
ngrok
</TextLink>
. Its free and easy to use and required to create a tunnel, making
your local machine accessible to the internet.
</Paragraph>
<StepNumber stepNumber="a" title="Install ngrok" />
<StepContentContainer>
<ClipboardField
variant="primary/medium"
className="mt-2"
value={`brew install ngrok/ngrok/ngrok`}
/>
</StepContentContainer>
<StepNumber stepNumber="b" title="Open a new terminal window/tab" />
<StepContentContainer>
<Paragraph spacing>You need to leave this running.</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="c" title="Create an http tunnel at port 3000" />
<StepContentContainer>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`ngrok http 3000`}
/>
</StepContentContainer>
<StepNumber
stepNumber="d"
title="Grab your forwarding address in the ngrok output"
/>
<StepContentContainer>
<img src={ngrok} className="mt-2 w-full" />
</StepContentContainer>
<StepNumber stepNumber="d" title="Use the forwarding URL" />
<StepContentContainer>
<Paragraph spacing>
Use the forwarding URL that ngrok gave you for the{" "}
<InlineCode>VERCEL_URL</InlineCode> environment variable.
</Paragraph>
</StepContentContainer>
</StepContentContainer>
<StepNumber stepNumber="3" title="Use the CLI" />
<StepContentContainer>
<Paragraph spacing>
Run this CLI command in a new terminal window.
</Paragraph>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`npx @trigger.dev/init -k ${devEnvironment?.apiKey} -t https://test-cloud.trigger.dev`}
/>
<Paragraph spacing>
The CLI will add Trigger.dev to your existing Next.js project, setup a
route and give you an example file.
</Paragraph>
<Paragraph>
Once you've run the CLI command, click Refresh to view your example
Job in the list.
</Paragraph>
<Button
variant="primary/medium"
className="mt-4"
LeadingIcon="refresh"
onClick={() => window.location.reload()}
>
Refresh
</Button>
</StepContentContainer>
</>
);
}
export function HowToRunATest() {
return (
<>
<StepNumber stepNumber="1" title="Step 1 title" />
<StepContentContainer>
<Paragraph>Content</Paragraph>
</StepContentContainer>
</>
);
}
export function HowToConnectAnIntegration() {
return (
<>
<StepNumber stepNumber="1" title="Select an API from the list" />
<StepContentContainer>
<Paragraph>
APIs marked with a
<span
className="mx-2 -mt-1 inline-flex"
aria-label="Trigger.dev Integration icon"
>
<IntegrationIcon />
</span>
are Trigger.dev Integrations. These Integrations make connecting to
the API easier by offering OAuth or API key authentication. All APIs
can also be used with fetch or an SDK.
</Paragraph>
<img src={integrationButton} className="mt-2 h-10" />
</StepContentContainer>
<StepNumber stepNumber="2" title="Choose how you want to connect" />
<StepContentContainer>
<Paragraph>
Follow the instructions for your chosen connection method in the
popover form. If no Integration exists yet, you can request one by
clicking the "I want an Integration" button.
</Paragraph>
</StepContentContainer>
<StepNumber
stepNumber="3"
title="Your connection will appear in the list"
/>
<StepContentContainer>
<Paragraph>
Once you've connected your API, it will appear in the list of
Integrations below. You can view details and manage your connection by
selecting it from the table.
</Paragraph>
</StepContentContainer>
<Callout
variant={"docs"}
href="https://trigger.dev/docs/integrations/introduction"
>
View the Integration docs page for more information on connecting an API
using an Integration or other method.
</Callout>
</>
);
}
export function HowToUseThisIntegration() {
return (
<>
<StepNumber stepNumber="1" title="Step 1 title" />
<StepContentContainer>
<Paragraph>Content</Paragraph>
</StepContentContainer>
</>
);
}
function StepContentContainer({ children }: { children: React.ReactNode }) {
return <div className="mb-6 ml-9 mt-1">{children}</div>;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

@@ -0,0 +1,48 @@
import {
ApiAuthenticationMethodApiKey,
Integration,
} from "~/services/externalApis/types";
import { Paragraph } from "../primitives/Paragraph";
import { CodeBlock } from "../code/CodeBlock";
import { InlineCode } from "../code/InlineCode";
import { Header1, Header2 } from "../primitives/Headers";
import { ClipboardField } from "../primitives/ClipboardField";
export function ApiKeyHelp({
integration,
apiAuth,
}: {
integration: Integration;
apiAuth: ApiAuthenticationMethodApiKey;
}) {
return (
<div className="mt-4">
<Header1 className="mb-2">
How to use {integration.name} with API keys
</Header1>
<Paragraph spacing>
You can use API keys to authenticate with {integration.name}. Your API
keys won't leave your server, we'll never see them.
</Paragraph>
<Paragraph spacing>
First install the <InlineCode>{integration.packageName}</InlineCode>{" "}
package using your preferred package manager. For example:
</Paragraph>
<ClipboardField
variant="secondary/medium"
value={`npm install ${integration.packageName}`}
className="mb-4"
/>
{apiAuth.help.samples.map((sample, i) => (
<div key={i}>
<Paragraph spacing>{sample.title}</Paragraph>
<CodeBlock
code={sample.code}
className="mb-4"
highlightedRanges={sample.highlight}
/>
</div>
))}
</div>
);
}
@@ -0,0 +1,149 @@
import React, { useState } from "react";
import {
ApiAuthenticationMethodApiKey,
Integration,
} from "~/services/externalApis/types";
import { docsIntegrationPath } from "~/utils/pathBuilder";
import { LinkButton } from "../primitives/Buttons";
import { Header2 } from "../primitives/Headers";
import { NamedIconInBox } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
import {
Sheet,
SheetBody,
SheetContent,
SheetHeader,
SheetTrigger,
} from "../primitives/Sheet";
import { ApiKeyHelp } from "./ApiKeyHelp";
import { CustomHelp } from "./CustomHelp";
import { SelectOAuthMethod } from "./SelectOAuthMethod";
type IntegrationMethod = "apikey" | "oauth2" | "custom";
export function ConnectToIntegrationSheet({
integration,
organizationId,
button,
className,
callbackUrl,
}: {
integration: Integration;
organizationId: string;
button: React.ReactNode;
callbackUrl: string;
className?: string;
}) {
const [integrationMethod, setIntegrationMethod] = useState<
IntegrationMethod | undefined
>(undefined);
const authMethods = Object.values(integration.authenticationMethods);
const hasApiKeyOption = authMethods.some((s) => s.type === "apikey");
const hasOAuth2Option = authMethods.some((s) => s.type === "oauth2");
return (
<Sheet>
<SheetTrigger className={className}>{button}</SheetTrigger>
<SheetContent size="lg">
<SheetHeader>
<NamedIconInBox name={integration.identifier} className="h-9 w-9" />
<div className="grow">
<Header2>{integration.name}</Header2>
{integration.description && (
<Paragraph variant="small">{integration.description}</Paragraph>
)}
</div>
<LinkButton
to={docsIntegrationPath(integration.identifier)}
variant="secondary/small"
LeadingIcon="docs"
>
View docs
</LinkButton>
</SheetHeader>
<SheetBody>
<Header2 className="mb-2">Choose an integration method</Header2>
<RadioGroup
name="method"
className="flex gap-2"
value={integrationMethod}
onValueChange={(v) => setIntegrationMethod(v as IntegrationMethod)}
>
{hasOAuth2Option && (
<RadioGroupItem
id="oauth2"
value="oauth2"
label="OAuth"
description="We handle OAuth for you or your users."
variant="description"
/>
)}
{hasApiKeyOption && (
<RadioGroupItem
id="apikey"
value="apikey"
label="API Key"
description="Use API keys in your code. They never leave your server."
variant="description"
/>
)}
<RadioGroupItem
id="custom"
value="custom"
label="Fetch/Existing SDK"
description={`Alternatively, use ${integration.name} without our integration.`}
variant="description"
/>
</RadioGroup>
{integrationMethod && (
<SelectedIntegrationMethod
integration={integration}
organizationId={organizationId}
method={integrationMethod}
callbackUrl={callbackUrl}
/>
)}
</SheetBody>
</SheetContent>
</Sheet>
);
}
function SelectedIntegrationMethod({
integration,
organizationId,
method,
callbackUrl,
}: {
integration: Integration;
organizationId: string;
method: IntegrationMethod;
callbackUrl: string;
}) {
const authMethods = Object.values(integration.authenticationMethods);
switch (method) {
case "apikey":
const apiAuth = authMethods.find((a) => a.type === "apikey");
if (!apiAuth) return null;
return (
<ApiKeyHelp
integration={integration}
apiAuth={apiAuth as ApiAuthenticationMethodApiKey}
/>
);
case "oauth2":
return (
<SelectOAuthMethod
integration={integration}
organizationId={organizationId}
callbackUrl={callbackUrl}
/>
);
case "custom":
return <CustomHelp name={integration.name} />;
}
}
@@ -0,0 +1,259 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher, useLocation, useNavigation } from "@remix-run/react";
import cuid from "cuid";
import { useState } from "react";
import simplur from "simplur";
import { useTextFilter } from "~/hooks/useTextFilter";
import { createSchema } from "~/routes/resources.connection.$organizationId.oauth2";
import {
ApiAuthenticationMethodOAuth2,
Integration,
Scope,
} from "~/services/externalApis/types";
import { cn } from "~/utils/cn";
import { CodeBlock } from "../code/CodeBlock";
import { Button } from "../primitives/Buttons";
import { Checkbox } from "../primitives/Checkbox";
import { Fieldset } from "../primitives/Fieldset";
import { FormError } from "../primitives/FormError";
import { Header2, Header3 } from "../primitives/Headers";
import { Input } from "../primitives/Input";
import { InputGroup } from "../primitives/InputGroup";
import { Label } from "../primitives/Label";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import type { ConnectionType } from "@trigger.dev/database";
import { useFeatures } from "~/hooks/useFeatures";
export type Status = "loading" | "idle";
export function ConnectToOAuthForm({
integration,
authMethod,
authMethodKey,
organizationId,
clientType,
callbackUrl,
}: {
integration: Integration;
authMethod: ApiAuthenticationMethodOAuth2;
authMethodKey: string;
organizationId: string;
clientType: ConnectionType;
callbackUrl: string;
}) {
const [id] = useState<string>(cuid());
const transition = useNavigation();
const fetcher = useFetcher();
const { isManagedCloud } = useFeatures();
const [
form,
{ title, scopes, hasCustomClient, customClientId, customClientSecret },
] = useForm({
lastSubmission: fetcher.data,
onValidate({ formData }) {
return parse(formData, {
// Create the schema without any constraint defined
schema: createSchema(),
});
},
});
const location = useLocation();
const [selectedScopes, setSelectedScopes] = useState<Set<string>>(
new Set(
authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name)
)
);
const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud;
const [useMyOAuthApp, setUseMyOAuthApp] = useState(requiresCustomOAuthApp);
const { filterText, setFilterText, filteredItems } = useTextFilter<Scope>({
items: authMethod.scopes,
filter: (scope, text) => {
if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
if (
scope.description &&
scope.description.toLowerCase().includes(text.toLowerCase())
)
return true;
return false;
},
});
return (
<fetcher.Form
method="post"
action={`/resources/connection/${organizationId}/oauth2`}
{...form.props}
className="flex h-full max-h-full flex-grow flex-col"
>
<Fieldset>
<input type="hidden" name="id" value={id} />
<input
type="hidden"
name="integrationIdentifier"
value={integration.identifier}
/>
<input
type="hidden"
name="integrationAuthMethod"
value={authMethodKey}
/>
<input type="hidden" name="redirectTo" value={location.pathname} />
<InputGroup>
<FormError>{form.error}</FormError>
</InputGroup>
<InputGroup fullWidth>
<Label variant="large">Name</Label>
<Input
type="text"
fullWidth
{...conform.input(title)}
placeholder={`e.g. Personal ${integration.name}`}
/>
<FormError>{title.error}</FormError>
</InputGroup>
<input type="hidden" name="clientType" value={clientType} />
<div>
<Header2>Use my OAuth App</Header2>
<Paragraph variant="small" className="mb-2">
To use your own OAuth app, check the option below and insert the
details.
</Paragraph>
<Checkbox
id="hasCustomClient"
label="Use my OAuth App"
variant="simple/small"
disabled={requiresCustomOAuthApp}
onChange={(checked) => setUseMyOAuthApp(checked)}
{...conform.input(hasCustomClient, { type: "checkbox" })}
defaultChecked={requiresCustomOAuthApp}
/>
{useMyOAuthApp && (
<div className="ml-6 mt-2">
<Paragraph variant="small" className="mb-2">
Set the callback url to{" "}
<CodeBlock code={callbackUrl} showLineNumbers={false} />
</Paragraph>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<InputGroup fullWidth>
<Label variant="small">Client ID</Label>
<Input
fullWidth
{...conform.input(customClientId, { type: "text" })}
/>
</InputGroup>
<InputGroup fullWidth>
<Label variant="small">Client secret</Label>
<Input
fullWidth
{...conform.input(customClientSecret, {
type: "password",
})}
/>
</InputGroup>
</div>
<FormError>{customClientId.error}</FormError>
</div>
</div>
)}
</div>
<div>
<Header2>Scopes</Header2>
<Paragraph variant="small" className="mb-4">
Select the scopes you want to grant to {integration.name} in order
for it to access your data. Note: If you try and perform an action
in a Job that requires a scope you havent granted, that task will
fail.
</Paragraph>
{/* <Header3 className="mb-2">
Select from popular scope collections
</Header3>
<fieldset>
<Checkbox
id="allScopes"
label="Select all"
variant="button/small"
/>
</fieldset> */}
<div className="mb-2 mt-4 flex items-center justify-between">
<Header3>Select {integration.name} scopes</Header3>
<Paragraph variant="small" className="text-slate-500">
{simplur`${selectedScopes.size} scope[|s] selected`}
</Paragraph>
</div>
<Input
placeholder="Search scopes"
className="mb-2"
variant="medium"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<div className="flex flex-col gap-y-0.5 overflow-hidden rounded-md">
{filteredItems.length === 0 && (
<Paragraph variant="small" className="p-4">
No scopes match {filterText}. Try a different search query.
</Paragraph>
)}
{authMethod.scopes.map((s) => {
return (
<Checkbox
key={s.name}
id={s.name}
value={s.name}
name="scopes"
label={s.name}
defaultChecked={s.defaultChecked ?? false}
badges={s.annotations?.map((a) => a.label)}
description={s.description}
variant="description"
className={cn(
filteredItems.find((f) => f.name === s.name) ? "" : "hidden"
)}
onChange={(isChecked) => {
if (isChecked) {
setSelectedScopes((selected) => {
selected.add(s.name);
return new Set(selected);
});
} else {
setSelectedScopes((selected) => {
selected.delete(s.name);
return new Set(selected);
});
}
}}
/>
);
})}
</div>
</div>
</Fieldset>
<div className="flex items-center justify-end gap-x-4">
<FormError>{scopes.error}</FormError>
<Button
type="submit"
className="flex gap-2"
disabled={transition.state !== "idle"}
variant="primary/medium"
>
<>
<NamedIcon name={integration.identifier} className={"h-4 w-4"} />
Connect to {integration.name}
</>
</Button>
</div>
</fetcher.Form>
);
}
@@ -0,0 +1,84 @@
import { CodeBlock } from "../code/CodeBlock";
import { Header1, Header2 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
export function CustomHelp({ name }: { name: string }) {
return (
<div className="mt-4">
<Header1 className="mb-2">
You can use any API with requests or an SDK
</Header1>
<Header2 className="mb-2">How to use an SDK</Header2>
<Paragraph spacing>
You can call SDK methods from inside the run function, but you should
wrap them in a Task to make sure they're resumable.
</Paragraph>
<Paragraph spacing>
Here's an example with the official GitHub SDK
</Paragraph>
<CodeBlock
code={`
new Job(client, {
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
trigger: cronTrigger({
cron: "*/5 * * * *", // every 5 minutes
}),
run: async (payload, io, ctx) => {
//wrap an SDK call in io.runTask so it's resumable and displays in logs
const repo = await io.runTask(
"Get repo",
//you can add metadata to the task to improve the display in the logs
{ name: "Get repo", icon: "github" },
async () => {
//this is the regular GitHub SDK
const response = await octokit.rest.repos.get({
owner: "triggerdotdev",
repo: "trigger.dev",
});
return response.data;
}
);
},
});
`}
highlightedRanges={[[9, 22]]}
className="mb-4"
/>
<Header2 className="mb-2">How to use fetch</Header2>
<Paragraph spacing>
You can use the fetch API to make requests to any API. Or a different
request library like axios if you'd prefer. Again wrapping the request
in a Task will make sure it's resumable.
</Paragraph>
<CodeBlock
code={`
new Job(client, {
id: "scheduled-job-1",
name: "Scheduled Job 1",
version: "0.1.1",
trigger: cronTrigger({
cron: "*/5 * * * *", // every 5 minutes
}),
run: async (payload, io, ctx) => {
//wrap anything in io.runTask so it's resumable and displays in logs
const repo = await io.runTask(
"Get org",
//you can add metadata to the task to improve the display in the logs
{ name: "Get org", icon: "github" },
async () => {
//you can use fetch, axios, or any other library to make requests
const response = await fetch('https://api.github.com/orgs/nodejs');
return response.json();
}
);
},
});
`}
highlightedRanges={[[9, 19]]}
className="mb-4"
/>
</div>
);
}
@@ -0,0 +1,75 @@
import React from "react";
import { Api } from "~/services/externalApis/apis";
import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { Header1 } from "../primitives/Headers";
import { NamedIconInBox } from "../primitives/NamedIcon";
import {
Sheet,
SheetBody,
SheetContent,
SheetHeader,
SheetTrigger,
} from "../primitives/Sheet";
import { CustomHelp } from "./CustomHelp";
import { CheckIcon } from "@heroicons/react/24/solid";
import { useFetcher } from "@remix-run/react";
import { Paragraph } from "../primitives/Paragraph";
export function NoIntegrationSheet({
api,
requested,
button,
}: {
api: Api;
requested: boolean;
button: React.ReactNode;
}) {
const fetcher = useFetcher();
const isLoading = fetcher.state !== "idle";
return (
<Sheet>
<SheetTrigger>{button}</SheetTrigger>
<SheetContent size="lg">
<SheetHeader className="justify-between">
<div className="flex items-center gap-4">
<NamedIconInBox name={api.identifier} className="h-9 w-9" />
<Header1>{api.name}</Header1>
</div>
{requested ? (
<div className="flex items-center gap-1">
<CheckIcon className="h-4 w-4 text-green-500" />
<Paragraph variant="small">
We'll let you know when the integration is available.
</Paragraph>
</div>
) : (
<fetcher.Form
method="post"
action={`/resources/apivote/${api.identifier}`}
>
<Button
variant="primary/small"
disabled={isLoading}
LeadingIcon={isLoading ? "spinner-white" : undefined}
>
{isLoading
? "Saving…"
: `I want an integration for ${api.name}`}
</Button>
</fetcher.Form>
)}
</SheetHeader>
<SheetBody>
<Callout variant="info">
We dont have an integration for the {api.name} API yet but you can
request one by clicking the button above. In the meantime, connect
to {api.name} using the methods below.
</Callout>
<CustomHelp name={api.name} />
</SheetBody>
</SheetContent>
</Sheet>
);
}
@@ -1,321 +0,0 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher, useLocation, useNavigation } from "@remix-run/react";
import cuid from "cuid";
import React, { useState } from "react";
import simplur from "simplur";
import { useTextFilter } from "~/hooks/useTextFilter";
import { createSchema } from "~/routes/resources.connection.$organizationId.oauth2";
import { Integration, Scope } from "~/services/externalApis/types";
import { cn } from "~/utils/cn";
import { CodeBlock } from "../code/CodeBlock";
import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { Checkbox } from "../primitives/Checkbox";
import { Fieldset } from "../primitives/Fieldset";
import { FormError } from "../primitives/FormError";
import { Header2, Header3 } from "../primitives/Headers";
import { Input } from "../primitives/Input";
import { InputGroup } from "../primitives/InputGroup";
import { Label } from "../primitives/Label";
import { NamedIcon, NamedIconInBox } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import {
Sheet,
SheetBody,
SheetContent,
SheetFooter,
SheetTrigger,
} from "../primitives/Sheet";
export type Status = "loading" | "idle";
export function OAuthConnectSheet({
integration,
authMethodKey,
organizationId,
button,
className,
}: {
integration: Integration;
authMethodKey: string;
organizationId: string;
button: React.ReactNode;
className?: string;
}) {
const [id] = useState<string>(cuid());
const transition = useNavigation();
const fetcher = useFetcher();
const [
form,
{ title, scopes, hasCustomClient, customClientId, customClientSecret },
] = useForm({
lastSubmission: fetcher.data,
onValidate({ formData }) {
return parse(formData, {
// Create the schema without any constraint defined
schema: createSchema(),
});
},
});
const apiAuthmethod = integration.authenticationMethods[authMethodKey];
const location = useLocation();
const options = [
{ label: "Your team", value: "DEVELOPER" },
{ label: "Your users", value: "EXTERNAL" },
];
const [selectedScopes, setSelectedScopes] = useState<Set<string>>(
new Set(
apiAuthmethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name)
)
);
const [useMyOAuthApp, setUseMyOAuthApp] = useState(false);
const [clientType, setClientType] = useState<"EXTERNAL" | "DEVELOPER">(
"DEVELOPER"
);
const { filterText, setFilterText, filteredItems } = useTextFilter<Scope>({
items: apiAuthmethod.scopes,
filter: (scope, text) => {
if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
if (
scope.description &&
scope.description.toLowerCase().includes(text.toLowerCase())
)
return true;
return false;
},
});
return (
<Sheet>
<SheetTrigger className={className}>{button}</SheetTrigger>
<SheetContent size="lg">
<fetcher.Form
method="post"
action={`/resources/connection/${organizationId}/oauth2`}
{...form.props}
className="flex h-full max-h-full flex-grow flex-col"
>
<SheetBody>
<div className="flex items-center gap-4 border-b border-slate-800 pb-3.5">
<NamedIconInBox
name={integration.identifier}
className="h-9 w-9"
/>
<div>
<Header2>Connect to {integration.name}</Header2>
<Paragraph variant="extra-small">
{apiAuthmethod.name}{" "}
{apiAuthmethod.description &&
` ${apiAuthmethod.description}`}
</Paragraph>
</div>
</div>
<Fieldset>
<input type="hidden" name="id" value={id} />
<input
type="hidden"
name="integrationIdentifier"
value={integration.identifier}
/>
<input
type="hidden"
name="integrationAuthMethod"
value={authMethodKey}
/>
<input
type="hidden"
name="redirectTo"
value={location.pathname}
/>
<InputGroup>
<FormError>{form.error}</FormError>
</InputGroup>
<InputGroup fullWidth>
<Label variant="large">Name</Label>
<Input
type="text"
fullWidth
{...conform.input(title)}
placeholder={`e.g. Personal ${integration.name}`}
/>
<FormError>{title.error}</FormError>
</InputGroup>
<Callout variant="info">
Coming soon create connections so you can run Jobs with your
users credentials.
</Callout>
{/* <div>
<Header2>Who will connect to this API?</Header2>
<Paragraph variant="small" className="mb-2">
Select Your team if you want create internal Jobs for your
own company. Select 'Your users' if you want to create Jobs
where your customers authenticate with this API.
</Paragraph>
<FormSegmentedControl
name="clientType"
defaultValue={clientType}
options={options}
onChange={(val) => setClientType(val as any)}
/>
</div> */}
<input type="hidden" name="clientType" value={"DEVELOPER"} />
<div>
<Header2>Use my OAuth App</Header2>
<Paragraph variant="small" className="mb-2">
To use your own OAuth app, check the option below and insert
the details.
</Paragraph>
<Checkbox
id="hasCustomClient"
label="Use my OAuth App"
variant="simple/small"
defaultChecked={
clientType === "EXTERNAL" ? true : useMyOAuthApp
}
disabled={clientType === "EXTERNAL"}
onChange={(checked) => setUseMyOAuthApp(checked)}
{...conform.input(hasCustomClient, { type: "checkbox" })}
/>
{useMyOAuthApp && (
<div className="ml-6 mt-2">
<Paragraph variant="small" className="mb-2">
Set the callback url to{" "}
<CodeBlock
code={`${origin}/oauth2/callback`}
showLineNumbers={false}
/>
</Paragraph>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<InputGroup fullWidth>
<Label variant="small">Client ID</Label>
<Input
fullWidth
{...conform.input(customClientId, { type: "text" })}
/>
</InputGroup>
<InputGroup fullWidth>
<Label variant="small">Client secret</Label>
<Input
fullWidth
{...conform.input(customClientSecret, {
type: "password",
})}
/>
</InputGroup>
</div>
<FormError>{customClientId.error}</FormError>
</div>
</div>
)}
</div>
<div>
<Header2>Scopes</Header2>
<Paragraph variant="small" className="mb-4">
Select the scopes you want to grant to {integration.name} in
order for it to access your data. Note: If you try and perform
an action in a Job that requires a scope you havent granted,
that task will fail.
</Paragraph>
{/* <Header3 className="mb-2">
Select from popular scope collections
</Header3>
<fieldset>
<Checkbox
id="allScopes"
label="Select all"
variant="button/small"
/>
</fieldset> */}
<div className="mb-2 mt-4 flex items-center justify-between">
<Header3>Select {integration.name} scopes</Header3>
<Paragraph variant="small" className="text-slate-500">
{simplur`${selectedScopes.size} scope[|s] selected`}
</Paragraph>
</div>
<Input
placeholder="Search scopes"
className="mb-2"
variant="medium"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<div className="flex flex-col gap-y-0.5 overflow-hidden rounded-md">
{filteredItems.length === 0 && (
<Paragraph variant="small" className="p-4">
No scopes match {filterText}. Try a different search
query.
</Paragraph>
)}
{apiAuthmethod.scopes.map((s) => {
return (
<Checkbox
key={s.name}
id={s.name}
value={s.name}
name="scopes"
label={s.name}
defaultChecked={s.defaultChecked ?? false}
badges={s.annotations?.map((a) => a.label)}
description={s.description}
variant="description"
className={cn(
filteredItems.find((f) => f.name === s.name)
? ""
: "hidden"
)}
onChange={(isChecked) => {
if (isChecked) {
setSelectedScopes((selected) => {
selected.add(s.name);
return new Set(selected);
});
} else {
setSelectedScopes((selected) => {
selected.delete(s.name);
return new Set(selected);
});
}
}}
/>
);
})}
</div>
</div>
</Fieldset>
</SheetBody>
<SheetFooter>
<div className="flex items-center justify-end gap-x-4">
<FormError>{scopes.error}</FormError>
<Button
type="submit"
className="flex gap-2"
disabled={transition.state !== "idle"}
variant="primary/medium"
>
<>
<NamedIcon
name={integration.identifier}
className={"h-4 w-4"}
/>
Connect to {integration.name}
</>
</Button>
</div>
</SheetFooter>
</fetcher.Form>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,120 @@
import { useState } from "react";
import {
ApiAuthenticationMethodOAuth2,
Integration,
} from "~/services/externalApis/types";
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
import type { ConnectionType } from "@trigger.dev/database";
import { Header2 } from "../primitives/Headers";
import { ConnectToOAuthForm } from "./ConnectToOAuthForm";
import { Paragraph } from "../primitives/Paragraph";
export function SelectOAuthMethod({
integration,
organizationId,
callbackUrl
}: {
integration: Integration;
organizationId: string;
callbackUrl: string;
}) {
const oAuthMethods = Object.entries(integration.authenticationMethods).filter(
(a): a is [string, ApiAuthenticationMethodOAuth2] => a[1].type === "oauth2"
);
const [oAuthKey, setOAuthKey] = useState<string | undefined>(
oAuthMethods.length === 1 ? oAuthMethods[0][0] : undefined
);
const [connectionType, setConnectionType] = useState<
ConnectionType | undefined
>();
const selectedOAuthMethod = oAuthKey
? (integration.authenticationMethods[
oAuthKey
] as ApiAuthenticationMethodOAuth2)
: undefined;
return (
<>
{oAuthMethods.length > 1 && (
<>
<Header2 className="mb-2 mt-4">Select an OAuth option</Header2>
<RadioGroup
name="oauth-method"
className="flex gap-2"
value={oAuthKey}
onValueChange={(v: any) => setOAuthKey(v)}
>
{oAuthMethods.map(([key, auth]) => (
<RadioGroupItem
key={key}
id={key}
value={key}
label={auth.name}
description={auth.description}
variant="description"
/>
))}
</RadioGroup>
</>
)}
{selectedOAuthMethod && (
<>
<Header2 className="mb-2 mt-4">
Who is connecting to {integration.name}
</Header2>
<RadioGroup
name="connection-type"
className="flex gap-2"
value={connectionType}
onValueChange={(v: any) => setConnectionType(v as ConnectionType)}
>
<RadioGroupItem
id="DEVELOPER"
value="DEVELOPER"
label="Developers"
description={`You will connect using an internal ${integration.name} account.`}
variant="description"
/>
<RadioGroupItem
id="EXTERNAL"
value="EXTERNAL"
label="Your users"
description="We will give you OAuth React components so you can connect as your users."
variant="description"
/>
</RadioGroup>
</>
)}
{selectedOAuthMethod &&
connectionType &&
oAuthKey &&
(connectionType === "DEVELOPER" ? (
<ConnectToOAuthForm
integration={integration}
authMethod={selectedOAuthMethod}
authMethodKey={oAuthKey}
organizationId={organizationId}
clientType={connectionType}
callbackUrl={callbackUrl}
/>
) : (
<>
<Header2 className="mb-1 mt-4">User OAuth coming soon</Header2>
<Paragraph spacing>
End-user OAuth is going to be released soon. If you are interested
in being an early beta tester then please{" "}
<a
href="mailto:founders@trigger.dev"
className="text-indigo-500 underline"
>
message us
</a>
.
</Paragraph>
</>
))}
</>
);
}
@@ -0,0 +1,10 @@
import type { ConnectionType } from "@trigger.dev/database";
export function connectionType(type: ConnectionType) {
switch (type) {
case "DEVELOPER":
return "Developer";
case "EXTERNAL":
return "Your users";
}
}
@@ -0,0 +1,168 @@
import { jobPath } from "~/utils/pathBuilder";
import { DateTime } from "../primitives/DateTime";
import { LabelValueStack } from "../primitives/LabelValueStack";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellChevron,
TableHeader,
TableHeaderCell,
TableRow,
} from "../primitives/Table";
import { SimpleTooltip } from "../primitives/Tooltip";
import { runStatusTitle } from "../runs/RunStatuses";
import { ProjectJob, useProject } from "~/hooks/useProject";
import { useOrganization } from "~/hooks/useOrganizations";
import { JobRunStatus } from "~/models/job.server";
export function JobsTable({
jobs,
noResultsText,
}: {
jobs: ProjectJob[];
noResultsText: string;
}) {
const organization = useOrganization();
const project = useProject();
return (
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Job</TableHeaderCell>
<TableHeaderCell>ID</TableHeaderCell>
<TableHeaderCell>Integrations</TableHeaderCell>
<TableHeaderCell>Properties</TableHeaderCell>
<TableHeaderCell>Last run</TableHeaderCell>
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{jobs.length > 0 ? (
jobs.map((job) => {
const path = jobPath(organization, project, job);
return (
<TableRow key={job.id}>
<TableCell to={path}>
<span className="flex items-center gap-2">
<NamedIcon name={job.event.icon} className="h-8 w-8" />
<LabelValueStack
label={job.title}
value={
job.dynamic ? (
<span className="flex items-center gap-0.5">
<NamedIcon name="dynamic" className="h-4 w-4" />{" "}
<span className="uppercase">Dynamic:</span>{" "}
{job.event.title}
</span>
) : (
job.event.title
)
}
variant="primary"
/>
</span>
</TableCell>
<TableCell to={path}>
<LabelValueStack
label={job.slug}
value={`v${job.version}`}
variant="primary"
/>
</TableCell>
<TableCell to={path}>
{job.integrations.map((integration) => (
<SimpleTooltip
key={integration.key}
button={
<NamedIcon
name={integration.icon}
className="h-6 w-6"
/>
}
content={`${integration.title}: ${integration.key}`}
/>
))}
</TableCell>
<TableCell to={path}>
{job.properties && (
<SimpleTooltip
button={
<div className="flex max-w-[200px] items-start justify-start gap-5 truncate">
{job.properties.map((property, index) => (
<LabelValueStack
key={index}
label={property.label}
value={property.text}
className=" last:truncate"
/>
))}
</div>
}
content={
<div className="flex flex-col gap-2">
{job.properties.map((property, index) => (
<LabelValueStack
key={index}
label={property.label}
value={property.text}
/>
))}
</div>
}
/>
)}
</TableCell>
<TableCell to={path}>
{job.lastRun ? (
<LabelValueStack
label={
<span className={classForJobStatus(job.lastRun.status)}>
{runStatusTitle(job.lastRun.status)}
</span>
}
value={
<DateTime
date={job.lastRun.createdAt}
className={classForJobStatus(job.lastRun.status)}
/>
}
/>
) : (
<LabelValueStack label={"Never run"} value={""} />
)}
</TableCell>
<TableCellChevron to={path} />
</TableRow>
);
})
) : (
<TableBlankRow colSpan={6}>
<Paragraph
variant="small"
className="flex items-center justify-center"
>
{noResultsText}
</Paragraph>
</TableBlankRow>
)}
</TableBody>
</Table>
);
}
function classForJobStatus(status: JobRunStatus) {
switch (status) {
case "FAILURE":
case "TIMED_OUT":
case "WAITING_ON_CONNECTIONS":
case "PENDING":
return "text-rose-500";
default:
return "";
}
}
@@ -64,6 +64,10 @@ export function PageBody({
);
}
export function PageBodyPadding({ children }: { children: React.ReactNode }) {
return <div className="p-4">{children}</div>;
}
export function MainCenteredContainer({
children,
}: {
@@ -1,31 +1,44 @@
import { useMatches } from "@remix-run/react";
import { Fragment } from "react";
import {
useIntegrationClient,
useOptionalIntegrationClient,
} from "~/hooks/useIntegrationClient";
import { useJob, useOptionalJob } from "~/hooks/useJob";
import {
useOptionalOrganization,
useOrganization,
} from "~/hooks/useOrganizations";
import { useOptionalProject, useProject } from "~/hooks/useProject";
import { useOptionalRun, useRun } from "~/hooks/useRun";
import {
integrationClientConnectionsPath,
integrationClientPath,
integrationClientScopesPath,
jobPath,
jobTestPath,
projectEnvironmentsPath,
projectIntegrationsPath,
projectPath,
runPath,
runDashboardPath,
} from "~/utils/pathBuilder";
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
import { JobsMenu } from "./JobsMenu";
import { BreadcrumbLink } from "./NavBar";
import { ProjectsMenu } from "./ProjectsMenu";
import { useOptionalRun, useRun } from "~/hooks/useRun";
export type Breadcrumb = {
slug:
| "projects"
| "jobs"
| "integrations"
| "integration"
| "integration-job"
| "integration-connections"
| "integration-scopes"
| "environments"
| "job"
| "test"
| "runs"
| "run";
link?: {
@@ -49,6 +62,7 @@ export function Breadcrumb() {
const project = useOptionalProject();
const job = useOptionalJob();
const run = useOptionalRun();
const client = useOptionalIntegrationClient();
return (
<div className="hidden items-center md:flex">
@@ -61,6 +75,7 @@ export function Breadcrumb() {
project={project}
job={job}
run={run}
client={client}
/>
</Fragment>
))}
@@ -74,12 +89,14 @@ function BreadcrumbItem({
project,
job,
run,
client,
}: {
breadcrumb: Breadcrumb;
organization?: ReturnType<typeof useOrganization>;
project?: ReturnType<typeof useProject>;
job?: ReturnType<typeof useJob>;
run?: ReturnType<typeof useRun>;
client?: ReturnType<typeof useIntegrationClient>;
}) {
switch (breadcrumb.slug) {
case "projects":
@@ -108,6 +125,45 @@ function BreadcrumbItem({
title="Integrations"
/>
);
case "integration":
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={projectIntegrationsPath(organization!, project!)}
title="Integrations"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={integrationClientPath(organization!, project!, client!)}
title={client!.title}
/>
</Fragment>
);
case "integration-job":
return (
<BreadcrumbLink
to={integrationClientPath(organization!, project!, client!)}
title={"Jobs"}
/>
);
case "integration-connections":
return (
<BreadcrumbLink
to={integrationClientConnectionsPath(
organization!,
project!,
client!
)}
title={"Connections"}
/>
);
case "integration-scopes":
return (
<BreadcrumbLink
to={integrationClientScopesPath(organization!, project!, client!)}
title={"Scopes"}
/>
);
case "job":
return (
<Fragment key={breadcrumb.slug}>
@@ -119,6 +175,14 @@ function BreadcrumbItem({
<JobsMenu key={breadcrumb.slug} />
</Fragment>
);
case "test":
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={jobTestPath(organization!, project!, job!)}
title="Test"
/>
);
case "runs":
return (
<BreadcrumbLink
@@ -136,7 +200,7 @@ function BreadcrumbItem({
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={runPath(organization!, project!, job!, run!)}
to={runDashboardPath(organization!, project!, job!, run!)}
title={`Run #${run!.number}`}
/>
</Fragment>
@@ -28,7 +28,11 @@ export function JobsMenu() {
<PopoverArrowTrigger isOpen={isOpen}>
{currentJob?.title ?? "Select a job"}
</PopoverArrowTrigger>
<PopoverContent className="w-80 p-0" align="start">
<PopoverContent
className="w-80 overflow-y-auto p-0"
align="start"
collisionPadding={20}
>
<PopoverSectionHeader title="Jobs" />
<div className="flex flex-col gap-1 p-1">
{project.jobs.map((job) => {
@@ -1,13 +1,13 @@
import { Popover, Transition } from "@headlessui/react";
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { Fragment } from "react";
import { cn } from "~/utils/cn";
import { Feedback } from "../Feedback";
import { LogoIcon } from "../LogoIcon";
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
import { Button, LinkButton } from "../primitives/Buttons";
import { LinkButton } from "../primitives/Buttons";
import { Breadcrumb } from "./Breadcrumb";
import { docsRoot } from "~/utils/pathBuilder";
export function NavBar() {
return (
@@ -21,20 +21,13 @@ export function NavBar() {
</div>
<div className="hidden items-center gap-2 sm:flex">
<LinkButton
to="https://docs.trigger.dev"
to={docsRoot()}
variant="secondary/small"
LeadingIcon={BookOpenIcon}
>
Documentation
</LinkButton>
<Button
variant="secondary/small"
data-attr="posthog-feedback-button"
LeadingIcon={ChatBubbleLeftRightIcon}
shortcut="F"
>
Send us feedback
</Button>
<Feedback />
</div>
</div>
);
@@ -83,7 +76,7 @@ function MobileDropdownMenu() {
className="absolute inset-x-6 top-0 mt-20 flex origin-top flex-col gap-4 rounded-md bg-slate-800 p-4 text-lg tracking-tight text-slate-900 shadow-xl ring-1 ring-slate-700"
>
{/* <PrimaryA
href="https://docs.trigger.dev"
href={docsRoot()}
target="_blank"
className="max-w-full"
>
@@ -17,6 +17,7 @@ import { NavLinkButton } from "../primitives/Buttons";
import type { IconNames } from "../primitives/NamedIcon";
import { SimpleTooltip } from "../primitives/Tooltip";
import { UserProfilePhoto } from "../UserProfilePhoto";
import { useOptionalIntegrationClient } from "~/hooks/useIntegrationClient";
export function SideMenuContainer({ children }: { children: React.ReactNode }) {
return (
@@ -26,14 +27,17 @@ export function SideMenuContainer({ children }: { children: React.ReactNode }) {
);
}
const expandedWith = "14rem";
const collapsedWith = "2.81rem";
const menuVariants = {
expanded: {
minWidth: "11rem",
width: "11rem",
minWidth: expandedWith,
width: expandedWith,
},
collapsed: {
minWidth: "2.81rem",
width: "2.81rem",
minWidth: collapsedWith,
width: collapsedWith,
},
};
@@ -44,7 +48,8 @@ export function ProjectSideMenu() {
//we collapse the menu if we're in a job or an integration
const job = useOptionalJob();
const isCollapsed = job !== undefined;
const client = useOptionalIntegrationClient();
const isCollapsed = job !== undefined || client !== undefined;
const jobsActive =
job !== undefined ||
@@ -75,7 +80,7 @@ export function ProjectSideMenu() {
isCollapsed={isCollapsed}
/>
<SideMenuItem
name="Environments"
name="Environments & API Keys"
icon="environment"
to={projectEnvironmentsPath(organization, project)}
isCollapsed={isCollapsed}
@@ -89,7 +94,7 @@ export function ProjectSideMenu() {
isCollapsed={isCollapsed}
/>
<SideMenuItem
name="Billing"
name="Usage & Billing"
icon="billing"
to={organizationBillingPath(organization)}
isCollapsed={isCollapsed}
@@ -149,7 +154,7 @@ function SideMenuItem({
}}
>
<motion.span
className="pl-1"
className="shrink-0 pl-1"
animate={isCollapsed ? "collapsed" : "expanded"}
variants={itemVariants}
initial={isCollapsed ? "collapsed" : "expanded"}
@@ -1,9 +1,14 @@
import { LinkProps, NavLink, NavLinkProps } from "@remix-run/react";
import { Link } from "@remix-run/react";
import React from "react";
import { Link, LinkProps, NavLink, NavLinkProps } from "@remix-run/react";
import React, {
ReactComponentElement,
forwardRef,
useImperativeHandle,
useRef,
} from "react";
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import type { IconNames } from "./NamedIcon";
import { NamedIcon } from "./NamedIcon";
import { ShortcutKey } from "./ShortcutKey";
const variant = {
"primary/small": {
@@ -12,8 +17,9 @@ const variant = {
button:
"h-6 px-[5px] text-xs bg-indigo-600 group-hover:bg-indigo-500/90 group-disabled:opacity-50 group-disabled:pointer-events-none",
icon: "h-3.5",
shortcutVariant: "small" as const,
shortcut:
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-bright/40 text-bright group-hover:border-bright/60 transition",
"ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60 justify-self-center",
},
"secondary/small": {
textColor:
@@ -21,8 +27,9 @@ const variant = {
button:
"h-6 px-[5px] text-xs bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50 group-disabled:pointer-events-none",
icon: "h-3.5",
shortcutVariant: "small" as const,
shortcut:
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition",
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
},
"tertiary/small": {
textColor:
@@ -30,8 +37,9 @@ const variant = {
button:
"h-6 px-[5px] text-xs bg-transparent group-hover:bg-slate-850 disabled:opacity-50 group-disabled:pointer-events-none",
icon: "h-3.5",
shortcutVariant: "small" as const,
shortcut:
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition",
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
},
"danger/small": {
textColor:
@@ -39,8 +47,9 @@ const variant = {
button:
"h-6 px-[5px] text-xs bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50 group-disabled:pointer-events-none",
icon: "h-3.5",
shortcutVariant: "small" as const,
shortcut:
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-bright/40 text-bright group-hover:border-bright/60 transition",
"ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"primary/medium": {
textColor:
@@ -48,8 +57,9 @@ const variant = {
button:
"h-8 px-2 text-sm bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
icon: "h-4",
shortcutVariant: "medium" as const,
shortcut:
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-bright group-hover:border-bright/60 transition",
"ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"secondary/medium": {
textColor:
@@ -57,8 +67,9 @@ const variant = {
button:
"h-8 px-2 text-sm bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
icon: "h-4",
shortcutVariant: "medium" as const,
shortcut:
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright transition",
"ml-1.5 -mr-0.5 border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright",
},
"tertiary/medium": {
textColor:
@@ -66,8 +77,9 @@ const variant = {
button:
"h-8 px-2 text-sm bg-transparent group-hover:bg-slate-850 disabled:opacity-50",
icon: "h-4",
shortcutVariant: "medium" as const,
shortcut:
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright transition",
"ml-1.5 -mr-0.5 border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright",
},
"danger/medium": {
textColor:
@@ -75,8 +87,9 @@ const variant = {
button:
"h-8 px-2 text-sm bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50",
icon: "h-4",
shortcutVariant: "medium" as const,
shortcut:
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-bright group-hover:border-bright/60 transition",
"ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"primary/large": {
textColor:
@@ -84,6 +97,7 @@ const variant = {
button:
"h-10 px-2 text-sm font-medium bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
icon: "h-5",
shortcutVariant: undefined,
shortcut: undefined,
},
"secondary/large": {
@@ -91,6 +105,7 @@ const variant = {
button:
"h-10 px-2 text-sm text-dimmed group-hover:text-bright transition font-medium bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
icon: "h-5",
shortcutVariant: undefined,
shortcut: undefined,
},
"menu-item": {
@@ -98,6 +113,7 @@ const variant = {
button:
"h-9 px-[0.475rem] text-sm rounded-sm bg-transparent group-hover:bg-slate-800 transition",
icon: "h-5",
shortcutVariant: undefined,
shortcut: undefined,
},
};
@@ -116,7 +132,7 @@ type ButtonContentPropsType = {
fullWidth?: boolean;
textAlignLeft?: boolean;
className?: string;
shortcut?: string;
shortcut?: ShortcutDefinition;
variant: keyof typeof variant;
};
@@ -132,15 +148,14 @@ export function ButtonContent(props: ButtonContentPropsType) {
textAlignLeft,
className,
} = props;
const variation = allVariants.variant[props.variant];
// Based on the size prop, we'll use the corresponding variant classnames
const btnClassName = cn(
allVariants.$all,
allVariants.variant[props.variant].button
);
const iconClassName = allVariants.variant[props.variant].icon;
const shortcutClassName = allVariants.variant[props.variant].shortcut;
const textColorClassName = allVariants.variant[props.variant].textColor;
const btnClassName = cn(allVariants.$all, variation.button);
const iconClassName = variation.icon;
const shortcutClassName = variation.shortcut;
const textColorClassName = variation.textColor;
return (
<div
className={cn(
@@ -181,7 +196,6 @@ export function ButtonContent(props: ButtonContentPropsType) {
<span
className={cn(
"mx-auto grow self-center truncate px-1",
textColorClassName
)}
>
@@ -211,7 +225,13 @@ export function ButtonContent(props: ButtonContentPropsType) {
)}
/>
))}
{shortcut && <span className={cn(shortcutClassName)}>{shortcut}</span>}
{shortcut && (
<ShortcutKey
className={cn(shortcutClassName)}
shortcut={shortcut}
variant={variation.shortcutVariant ?? "medium"}
/>
)}
</div>
</div>
);
@@ -223,8 +243,23 @@ type ButtonPropsType = Pick<
> &
React.ComponentProps<typeof ButtonContent>;
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
({ type, disabled, onClick, ...props }, ref) => {
const innerRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement);
if (props.shortcut) {
useShortcutKeys({
shortcut: props.shortcut,
action: () => {
if (innerRef.current) {
innerRef.current.click();
}
},
disabled,
});
}
return (
<button
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
@@ -233,7 +268,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
onClick={onClick}
name={props.name}
value={props.value}
ref={ref}
ref={innerRef}
form={props.form}
>
<ButtonContent {...props} />
@@ -245,10 +280,23 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
type LinkPropsType = Pick<LinkProps, "to" | "target"> &
React.ComponentProps<typeof ButtonContent>;
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
const innerRef = useRef<HTMLAnchorElement>(null);
if (props.shortcut) {
useShortcutKeys({
shortcut: props.shortcut,
action: () => {
if (innerRef.current) {
innerRef.current.click();
}
},
});
}
if (to.toString().startsWith("http")) {
return (
<ExtLink
href={to.toString()}
ref={innerRef}
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
>
<ButtonContent {...props} />
@@ -258,6 +306,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
return (
<Link
to={to}
ref={innerRef}
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
>
<ButtonContent {...props} />
@@ -299,16 +348,19 @@ type ExtLinkProps = JSX.IntrinsicElements["a"] & {
href: string;
};
function ExtLink({ className, href, children, ...props }: ExtLinkProps) {
return (
<a
className={cn(className)}
target="_blank"
rel="noopener noreferrer"
href={href}
{...props}
>
{children}
</a>
);
}
const ExtLink = forwardRef<HTMLAnchorElement, ExtLinkProps>(
({ className, href, children, ...props }, ref) => {
return (
<a
className={cn(className)}
target="_blank"
rel="noopener noreferrer"
href={href}
ref={ref}
{...props}
>
{children}
</a>
);
}
);
@@ -6,28 +6,48 @@ import {
} from "@heroicons/react/20/solid";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import {
ArrowTopRightOnSquareIcon,
BookOpenIcon,
CheckCircleIcon,
} from "@heroicons/react/24/solid";
const variantClasses = {
info: {
className: "border-blue-400/20 bg-blue-400/30",
icon: <InformationCircleIcon className="h-5 w-5 text-blue-400" />,
icon: <InformationCircleIcon className="h-5 w-5 shrink-0 text-blue-400" />,
text: "text-blue-200",
},
warning: {
className: "border-yellow-400/20 bg-yellow-400/30",
icon: <ExclamationCircleIcon className="h-5 w-5 text-yellow-400" />,
icon: (
<ExclamationCircleIcon className="h-5 w-5 shrink-0 text-yellow-400" />
),
text: "text-yellow-200",
},
error: {
className: "border-[hsl(347,77%,50%)]/30 bg-[hsl(347,77%,50%)]/20",
icon: <ExclamationTriangleIcon className="h-5 w-5 text-rose-400" />,
icon: (
<ExclamationTriangleIcon className="h-5 w-5 shrink-0 text-rose-400" />
),
text: "text-rose-200",
},
idea: {
className: "border-green-400/20 bg-green-400/30",
icon: <LightBulbIcon className="h-5 w-5 text-green-400" />,
icon: <LightBulbIcon className="h-5 w-5 shrink-0 text-green-400" />,
text: "text-green-200",
},
success: {
className: "border-green-400/20 bg-green-400/30",
icon: <CheckCircleIcon className="h-5 w-5 shrink-0 text-green-400" />,
text: "text-green-200",
},
docs: {
className:
"border-blue-400/20 bg-blue-400/30 transition hover:bg-blue-400/40",
icon: <BookOpenIcon className="h-5 w-5 shrink-0 text-blue-400" />,
text: "text-blue-200",
},
} as const;
export function Callout({
@@ -35,33 +55,60 @@ export function Callout({
className,
icon,
variant,
href,
}: {
children?: React.ReactNode;
className?: string;
icon?: React.ReactNode;
variant: keyof typeof variantClasses;
href?: string;
}) {
const variantDefinition = variantClasses[variant];
if (variant === "docs") {
return (
<a
href={href}
target="_blank"
className={cn(
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
variantDefinition.className,
className
)}
>
<div className="flex w-full items-center gap-x-2">
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.text}>
{children}
</Paragraph>
) : (
children
)}
</div>
<ArrowTopRightOnSquareIcon className="h-5 w-5 text-blue-400" />
</a>
);
}
return (
<div
className={cn(
`flex w-full gap-4 rounded-md border py-3 pl-3 pr-4 shadow-md backdrop-blur-sm`,
`flex w-full items-start gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
variantDefinition.className,
className
)}
>
<div className="flex items-start justify-start gap-2.5">
{icon ? icon : variantDefinition.icon}
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.text}>
{children}
</Paragraph>
) : (
children
)}
</div>
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.text}>
{children}
</Paragraph>
) : (
children
)}
</div>
);
}
@@ -41,7 +41,7 @@ const variants = {
},
description: {
button: "w-full py-2 pl-3 pr-4 checked:hover:bg-slate-850 transition",
label: "text-bright font-mono",
label: "text-bright font-semibold",
description: "text-dimmed",
inputPosition: "mt-1",
isChecked: "bg-slate-850",
@@ -59,7 +59,6 @@ export type CheckboxProps = Omit<
variant?: keyof typeof variants;
label?: string;
description?: string;
badge?: string;
badges?: string[];
className?: string;
onChange?: (isChecked: boolean) => void;
@@ -76,7 +75,6 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
label,
description,
defaultChecked,
badge,
badges,
disabled,
className,
@@ -0,0 +1,148 @@
import { cn } from "~/utils/cn";
import { Button } from "./Buttons";
import { useCallback, useRef, useState } from "react";
import { CheckIcon } from "@heroicons/react/20/solid";
import { IconNames, NamedIcon } from "./NamedIcon";
const variations = {
"primary/small": {
container:
"flex items-center text-dimmed font-mono rounded border border-slate-800 bg-slate-850 text-xs transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-slate-850 border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "primary/small" as const,
button: "rounded-l-none min-w-[3.1rem]",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"secondary/small": {
container:
"flex items-center text-dimmed font-mono rounded border border-slate-750 bg-slate-850 text-xs transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-slate-850 border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "secondary/small" as const,
button: "rounded-l-none border-l border-slate-750 min-w-[3.1rem]",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"tertiary/small": {
container:
"flex items-center text-dimmed font-mono rounded border border-slate-850 bg-transparent text-xs transition hover:border-slate-800 hover:bg-slate-950 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
buttonVariant: "tertiary/small" as const,
button: "rounded-l-none border-l border-slate-850 min-w-[3.1rem]",
iconSize: "h-3 w-3",
iconPadding: "pl-1",
},
"primary/medium": {
container:
"flex items-center text-dimmed font-mono rounded border border-slate-800 bg-slate-850 text-sm transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-slate-850 border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "primary/medium" as const,
button: "rounded-l-none min-w-[4rem]",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
"secondary/medium": {
container:
"flex items-center text-dimmed font-mono rounded border border-slate-750 bg-slate-850 text-sm transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
input:
"bg-slate-850 border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "secondary/medium" as const,
button: "rounded-l-none border-l border-slate-750 min-w-[4rem]",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
"tertiary/medium": {
container:
"flex items-center text-dimmed font-mono rounded border border-slate-850 bg-transparent text-sm transition hover:border-slate-800 hover:bg-slate-950 focus:border-4 focus:border-solid focus:border-l-transparent",
input:
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
buttonVariant: "tertiary/medium" as const,
button: "rounded-l-none border-l border-slate-850 min-w-[4rem]",
iconSize: "h-4 w-4",
iconPadding: "pl-2",
},
};
type ClipboardFieldProps = {
value: string;
secure?: boolean;
variant: keyof typeof variations;
className?: string;
icon?: IconNames | React.ReactNode;
};
export function ClipboardField({
value,
secure = false,
variant,
className,
icon,
}: ClipboardFieldProps) {
const [isSecure, setIsSecure] = useState(secure);
const [copied, setCopied] = useState(false);
const copy = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1500);
},
[value]
);
const { container, input, buttonVariant, button } = variations[variant];
const iconClassName = variations[variant].iconSize;
const iconPosition = variations[variant].iconPadding;
const inputIcon = useRef<HTMLInputElement>(null);
return (
<div className={cn(container, className)}>
{icon && (
<div
onClick={() => inputIcon.current && inputIcon.current.focus()}
className={cn(iconPosition, "flex items-center")}
>
{typeof icon === "string" ? (
<NamedIcon name={icon} className={iconClassName} />
) : (
icon
)}
</div>
)}
<input
type="text"
ref={inputIcon}
value={isSecure ? "•".repeat(value.length) : value}
readOnly={true}
className={cn("shrink grow select-all overflow-x-auto", input)}
size={value.length}
onFocus={(e) => {
if (secure) {
setIsSecure((i) => false);
}
e.currentTarget.select();
}}
onBlur={() => {
if (secure) {
setIsSecure((i) => true);
}
}}
/>
<Button
variant={buttonVariant}
onClick={copy}
className={cn("shrink-0 grow-0", button)}
>
{copied ? <CheckIcon className="h-4 w-4 text-green-500" /> : "Copy"}
</Button>
</div>
);
}
@@ -1,12 +1,16 @@
import { cn } from "~/utils/cn";
export function FormButtons({
cancelButton,
confirmButton,
className,
}: {
cancelButton?: React.ReactNode;
confirmButton: React.ReactNode;
className?: string;
}) {
return (
<div className="flex w-full items-center justify-between">
<div className={cn("flex w-full items-center justify-between", className)}>
{cancelButton ? cancelButton : <div />} {confirmButton}
</div>
);
@@ -22,9 +22,13 @@ const HelpContext = React.createContext<HelpContextValue>({
type HelpProps = {
defaultOpen?: boolean;
allowDismissing?: boolean;
children?: React.ReactNode;
children?: React.ReactNode | ((open: boolean) => React.ReactNode);
};
function useHelp() {
return React.useContext(HelpContext);
}
export function Help({
defaultOpen,
allowDismissing = true,
@@ -34,13 +38,13 @@ export function Help({
return (
<HelpContext.Provider value={{ open, setOpen, allowDismissing }}>
{children}
{typeof children === "function" ? children(open) : children}
</HelpContext.Provider>
);
}
export function HelpTrigger({ title }: { title: string }) {
const { open, setOpen } = React.useContext(HelpContext);
const { open, setOpen } = useHelp();
return open ? (
<></>
@@ -65,7 +69,7 @@ export function HelpContent({
className?: string;
children: React.ReactNode;
}) {
const { open, setOpen, allowDismissing } = React.useContext(HelpContext);
const { open, setOpen, allowDismissing } = useHelp();
return (
<>
@@ -89,7 +93,7 @@ export function HelpContent({
</div>
<div
className="grow rounded border border-slate-850 bg-midnight-850 bg-contain bg-left-top bg-no-repeat p-4"
className="grow rounded border border-slate-850 bg-midnight-850 bg-contain bg-left-top bg-no-repeat p-6"
style={{
backgroundImage: `url(${gradientPath})`,
}}
@@ -40,12 +40,13 @@ export function LabelValueStack({
<div
className={cn(
"flex items-baseline",
layout === "vertical" ? "flex-col" : "gap-1",
layout === "vertical" && "flex-col",
variant === "primary" ? "gap-x-1 gap-y-0" : "gap-x-1 gap-y-0.5",
className
)}
>
<Paragraph variant={variation.label}>{label}</Paragraph>
<div>
<>
{href ? (
<SimpleTooltip
side="bottom"
@@ -53,7 +54,7 @@ export function LabelValueStack({
<Paragraph variant={variation.value}>
<a
href={href}
className=" underline underline-offset-2"
className="underline underline-offset-2"
target="_blank"
>
{value}
@@ -66,7 +67,7 @@ export function LabelValueStack({
) : (
<Paragraph variant={variation.value}>{value}</Paragraph>
)}
</div>
</>
</div>
);
}
@@ -7,9 +7,12 @@ import {
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
import {
ArrowLeftIcon,
ArrowPathIcon,
ArrowRightIcon,
ArrowTopRightOnSquareIcon,
BeakerIcon,
BellAlertIcon,
BoltIcon,
BookOpenIcon,
BuildingOffice2Icon,
CalendarDaysIcon,
@@ -20,6 +23,7 @@ import {
ChevronRightIcon,
ChevronUpIcon,
ClockIcon,
CloudIcon,
CodeBracketSquareIcon,
Cog8ToothIcon,
CreditCardIcon,
@@ -49,6 +53,8 @@ import { WebhookIcon } from "~/assets/icons/WebhookIcon";
import { cn } from "~/utils/cn";
import { LogoIcon } from "../LogoIcon";
import { Spinner } from "./Spinner";
import { HourglassIcon } from "lucide-react";
import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon";
const icons = {
account: (className: string) => (
@@ -60,6 +66,9 @@ const icons = {
"arrow-left": (className: string) => (
<ArrowLeftIcon className={cn("text-white", className)} />
),
background: (className: string) => (
<CloudIcon className={cn("text-sky-400", className)} />
),
beaker: (className: string) => (
<BeakerIcon className={cn("text-purple-500", className)} />
),
@@ -84,6 +93,9 @@ const icons = {
"chevron-right": (className: string) => (
<ChevronRightIcon className={cn("text-dimmed", className)} />
),
countdown: (className: string) => (
<HourglassIcon className={cn("text-amber-400", className)} />
),
clock: (className: string) => (
<ClockIcon className={cn("text-cyan-500", className)} />
),
@@ -96,9 +108,15 @@ const icons = {
docs: (className: string) => (
<BookOpenIcon className={cn("text-slate-400", className)} />
),
dynamic: (className: string) => (
<DynamicTriggerIcon className={cn("text-cyan-500", className)} />
),
error: (className: string) => (
<ExclamationCircleIcon className={cn("text-rose-500", className)} />
),
"external-link": (className: string) => (
<ArrowTopRightOnSquareIcon className={cn("text-dimmed", className)} />
),
flag: (className: string) => (
<FlagIcon className={cn("text-sky-500", className)} />
),
@@ -144,12 +162,10 @@ const icons = {
log: (className: string) => (
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
),
"logo-icon": (className: string) => <LogoIcon className={cn(className)} />,
organization: (className: string) => (
<BuildingOffice2Icon className={cn("text-fuchsia-600", className)} />
),
search: (className: string) => (
<MagnifyingGlassIcon className={cn("text-dimmed", className)} />
),
plus: (className: string) => (
<PlusIcon className={cn("text-green-600", className)} />
),
@@ -159,6 +175,15 @@ const icons = {
property: (className: string) => (
<Cog8ToothIcon className={cn("text-slate-600", className)} />
),
refresh: (className: string) => (
<ArrowPathIcon className={cn("text-bright", className)} />
),
search: (className: string) => (
<MagnifyingGlassIcon className={cn("text-dimmed", className)} />
),
settings: (className: string) => (
<Cog8ToothIcon className={cn("text-slate-600", className)} />
),
spinner: (className: string) => (
<Spinner className={className} color="blue" />
),
@@ -171,7 +196,9 @@ const icons = {
team: (className: string) => (
<UserGroupIcon className={cn("text-blue-500", className)} />
),
"logo-icon": (className: string) => <LogoIcon className={cn(className)} />,
trigger: (className: string) => (
<BoltIcon className={cn("text-orange-500", className)} />
),
user: (className: string) => (
<UserIcon className={cn("text-blue-600", className)} />
),
@@ -224,6 +251,9 @@ export function NamedIcon({
name={name}
className={"h-full w-full p-[7%]"}
variant="light"
style={{
shapeRendering: "geometricPrecision",
}}
/>
</div>
);
@@ -0,0 +1,34 @@
import type { ReactNode } from "react";
import { useContext } from "react";
import { createContext } from "react";
export type OperatingSystemPlatform = "mac" | "windows";
type OperatingSystemContext = {
platform: OperatingSystemPlatform;
};
type OperatingSystemContextProviderProps = {
platform: OperatingSystemPlatform;
children: ReactNode;
};
const Context = createContext<OperatingSystemContext | null>(null);
export const OperatingSystemContextProvider = ({
platform,
children,
}: OperatingSystemContextProviderProps) => {
return <Context.Provider value={{ platform }}>{children}</Context.Provider>;
};
const throwIfNoProvider = () => {
throw new Error(
"Please wrap your application in an OperatingSystemContextProvider."
);
};
export const useOperatingSystem = () => {
const { platform } = useContext(Context) ?? throwIfNoProvider();
return { platform };
};
@@ -65,7 +65,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
}
export function PageButtons({ children }: WithChildren) {
return <div className="flex gap-2">{children}</div>;
return <div className="flex items-center gap-3">{children}</div>;
}
export function PageDescription({ children }: WithChildren) {
@@ -102,7 +102,7 @@ export function PageInfoProperty({
value,
}: {
icon?: string | React.ReactNode;
label: string;
label?: string;
value: React.ReactNode;
}) {
return (
@@ -112,12 +112,14 @@ export function PageInfoProperty({
) : (
icon
)}
<Paragraph
variant="extra-small/caps"
className="mt-0.5 whitespace-nowrap"
>
{label}:
</Paragraph>
{label && (
<Paragraph
variant="extra-small/caps"
className="mt-0.5 whitespace-nowrap"
>
{label}:
</Paragraph>
)}
{typeof value === "string" || typeof value === "number" ? (
<Paragraph variant="small">{value}</Paragraph>
) : (
@@ -1,24 +1,62 @@
import { cn } from "~/utils/cn";
const paragraphVariants = {
base: "font-sans text-base font-normal text-dimmed",
"base/bright": "font-sans text-base font-normal text-bright",
small: "font-sans text-sm font-normal text-dimmed",
"small/bright": "font-sans text-sm font-normal text-bright",
"extra-small": "font-sans text-xs font-normal text-dimmed",
"extra-small/bright": "font-sans text-xs font-normal text-bright",
"extra-small/mono": "font-mono text-xs font-normal text-dimmed",
"extra-small/bright/mono": "font-mono text-xs text-bright",
"extra-small/caps":
"font-sans text-xs uppercase tracking-wider font-normal text-dimmed",
"extra-small/bright/caps":
"font-sans text-xs uppercase tracking-wider font-normal text-bright",
"extra-extra-small": "font-sans text-xxs font-normal text-dimmed",
"extra-extra-small/bright": "font-sans text-xxs font-normal text-bright",
"extra-extra-small/caps":
"font-sans text-xxs uppercase tracking-wider font-normal text-dimmed",
"extra-extra-small/bright/caps":
"font-sans text-xxs uppercase tracking-wider font-normal text-bright",
base: {
text: "font-sans text-base font-normal text-dimmed",
spacing: "mb-3",
},
"base/bright": {
text: "font-sans text-base font-normal text-bright",
spacing: "mb-3",
},
small: {
text: "font-sans text-sm font-normal text-dimmed",
spacing: "mb-2",
},
"small/bright": {
text: "font-sans text-sm font-normal text-bright",
spacing: "mb-2",
},
"extra-small": {
text: "font-sans text-xs font-normal text-dimmed",
spacing: "mb-1.5",
},
"extra-small/bright": {
text: "font-sans text-xs font-normal text-bright",
spacing: "mb-1.5",
},
"extra-small/mono": {
text: "font-mono text-xs font-normal text-dimmed",
spacing: "mb-1.5",
},
"extra-small/bright/mono": {
text: "font-mono text-xs text-bright",
spacing: "mb-1.5",
},
"extra-small/caps": {
text: "font-sans text-xs uppercase tracking-wider font-normal text-dimmed",
spacing: "mb-1.5",
},
"extra-small/bright/caps": {
text: "font-sans text-xs uppercase tracking-wider font-normal text-bright",
spacing: "mb-1.5",
},
"extra-extra-small": {
text: "font-sans text-xxs font-normal text-dimmed",
spacing: "mb-1",
},
"extra-extra-small/bright": {
text: "font-sans text-xxs font-normal text-bright",
spacing: "mb-1",
},
"extra-extra-small/caps": {
text: "font-sans text-xxs uppercase tracking-wider font-normal text-dimmed",
spacing: "mb-1",
},
"extra-extra-small/bright/caps": {
text: "font-sans text-xxs uppercase tracking-wider font-normal text-bright",
spacing: "mb-1",
},
};
export type ParagraphVariant = keyof typeof paragraphVariants;
@@ -26,17 +64,26 @@ export type ParagraphVariant = keyof typeof paragraphVariants;
type ParagraphProps = {
variant?: ParagraphVariant;
className?: string;
spacing?: boolean;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLParagraphElement>;
export function Paragraph({
variant = "base",
className,
spacing = false,
children,
...props
}: ParagraphProps) {
return (
<p className={cn(paragraphVariants[variant], className)} {...props}>
<p
className={cn(
paragraphVariants[variant].text,
spacing === true && paragraphVariants[variant].spacing,
className
)}
{...props}
>
{children}
</p>
);
@@ -0,0 +1,130 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "~/utils/cn";
import { Badge } from "./Badge";
import { Paragraph } from "./Paragraph";
const variants = {
"simple/small": {
button: "w-fit pr-4 data-[disabled]:opacity-70",
label: "text-sm text-bright mt-0.5 select-none",
description: "text-dimmed",
inputPosition: "mt-1",
},
simple: {
button: "w-fit pr-4 data-[disabled]:opacity-70",
label: "text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-1",
},
"button/small": {
button:
"flex items-center w-fit h-8 pl-2 pr-3 rounded border border-slate-800 hover:bg-slate-850 hover:border-slate-750 transition data-[disabled]:opacity-70 data-[disabled]:hover:bg-transparent data-[state=checked]:bg-slate-850",
label: "text-sm text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-0",
},
button: {
button:
"w-fit py-2 pl-3 pr-4 rounded border border-slate-800 hover:bg-slate-850 hover:border-slate-750 transition data-[state=checked]:bg-slate-850 data-[disabled]:opacity-70",
label: "text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-1",
},
description: {
button:
"w-full py-2 pl-2 pr-3 hover:bg-slate-850 transition data-[disabled]:opacity-70 data-[state=checked]:bg-slate-850 border-slate-800 border rounded-sm",
label: "text-bright font-semibold -mt-1 text-left",
description: "text-dimmed -mt-0.5 text-left",
inputPosition: "mt-0",
},
};
export const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root className={className} {...props} ref={ref} />
);
});
type RadioGroupItemProps = Omit<
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
"onChange"
> & {
variant?: keyof typeof variants;
label?: string;
description?: string;
badges?: string[];
className?: string;
};
export const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
RadioGroupItemProps
>(
(
{
className,
children,
variant = "simple",
label,
description,
badges,
...props
},
ref
) => {
const variation = variants[variant];
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"group flex cursor-pointer items-start gap-x-2 transition",
variation.button,
className
)}
{...props}
>
<div
className={cn(
"aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-sm border border-slate-700 ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
variation.inputPosition
)}
>
<RadioGroupPrimitive.Indicator className="flex h-full w-full items-center justify-center bg-indigo-700">
<Circle className="h-1.5 w-1.5 fill-white text-white" />
</RadioGroupPrimitive.Indicator>
</div>
<div>
<div className="flex items-center gap-x-2">
<label
htmlFor={props.id}
className={cn("cursor-pointer", variation.label)}
>
{label}
</label>
{badges && (
<span className="-mr-2 flex gap-x-1.5">
{badges.map((badge) => (
<Badge key={badge}>{badge}</Badge>
))}
</span>
)}
</div>
{variant === "description" && (
<Paragraph
variant="small"
className={cn("mt-0.5", variation.description)}
>
{description}
</Paragraph>
)}
</div>
</RadioGroupPrimitive.Item>
);
}
);
@@ -104,7 +104,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50 relative my-0.5 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition first-of-type:my-1 last-of-type:my-1 hover:bg-slate-750 focus:bg-slate-750/50",
"relative my-0.5 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition first-of-type:my-1 last-of-type:my-1 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-slate-750 focus:bg-slate-750/50",
className
)}
{...props}
@@ -1,10 +1,9 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "~/utils/cn";
import { NamedIcon } from "./NamedIcon";
import { ShortcutKey } from "./ShortcutKey";
@@ -159,15 +158,17 @@ const SheetContent = React.forwardRef<
className={cn(sheetVariants({ position, size }), className)}
{...props}
>
<div className="grid h-full grid-rows-[2.75rem_1fr]">
<div className="grid max-h-full grid-rows-[2.75rem_1fr] overflow-hidden">
<div className="flex items-center gap-2 border-b border-slate-800 p-2">
<SheetPrimitive.Close className="rounded-sm p-1 transition hover:bg-slate-800 disabled:pointer-events-none">
<NamedIcon name="close" className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
<ShortcutKey shortcut="esc" variant="medium" />
<ShortcutKey shortcut={{ key: "esc" }} variant="small" />
</div>
<div className="flex max-h-full flex-col overflow-hidden">
{children}
</div>
<div className="overflow-hidden">{children}</div>
</div>
</SheetPrimitive.Content>
</SheetPortal>
@@ -180,7 +181,20 @@ export const SheetBody = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"grow overflow-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
"overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
className
)}
{...props}
/>
);
export const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"mx-4 flex shrink-0 items-center gap-4 border-b border-slate-800 py-3.5",
className
)}
{...props}
@@ -197,4 +211,4 @@ export const SheetFooter = ({
</div>
);
export { Sheet, SheetTrigger, SheetContent };
export { Sheet, SheetContent, SheetTrigger };
@@ -1,16 +1,19 @@
import { Fragment } from "react";
import { Modifier, ShortcutDefinition } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { useOperatingSystem } from "./OperatingSystemProvider";
const variants = {
small:
"text-xxs font-medium py-0.25 min-w-[14px] rounded-[2px] px-0.5 border border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright transition",
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition uppercase",
medium:
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] border border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright transition",
"text-[0.75rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition uppercase",
};
export type ShortcutKeyVariant = keyof typeof variants;
type ShortcutKeyProps = {
shortcut: string;
shortcut: ShortcutDefinition;
variant: ShortcutKeyVariant;
className?: string;
};
@@ -20,5 +23,32 @@ export function ShortcutKey({
variant,
className,
}: ShortcutKeyProps) {
return <span className={cn(variants[variant], className)}>{shortcut}</span>;
const { platform } = useOperatingSystem();
const isMac = platform === "mac";
let relevantShortcut =
"mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
const modifiers = relevantShortcut.modifiers ?? [];
const character = relevantShortcut.key;
return (
<span className={cn(variants[variant], className)}>
{modifiers.map((k) => (
<Fragment key={k}>{modifierString(k, isMac)}</Fragment>
))}
{character}
</span>
);
}
function modifierString(modifier: Modifier, isMac: boolean) {
switch (modifier) {
case "alt":
return isMac ? "⌥" : "Alt+";
case "ctrl":
return isMac ? "⌃" : "Ctrl+";
case "meta":
return isMac ? "⌘" : "⊞";
case "shift":
return isMac ? "⇧" : "Shift+";
}
}
@@ -1,30 +1,32 @@
export function TutorialStep({
import { Header2 } from "./Headers";
export function StepNumber({
stepNumber,
drawLine,
active = false,
complete = false,
title,
}: {
stepNumber?: string;
drawLine?: boolean;
active?: boolean;
complete?: boolean;
title?: string;
}) {
return (
<div className="mr-3 flex flex-col items-center justify-center">
<div className="mr-3">
{active ? (
<span className="flex h-7 w-7 items-center justify-center rounded bg-green-600 py-1 text-sm font-semibold text-slate-900 shadow">
{stepNumber}
</span>
<div className="flex items-center gap-x-3">
<span className="flex h-6 w-6 items-center justify-center rounded bg-green-600 py-1 text-xs font-semibold text-slate-900 shadow">
{stepNumber}
</span>
<Header2>{title}</Header2>
</div>
) : (
<span className="flex h-7 w-7 items-center justify-center rounded border border-slate-700 bg-slate-800 py-1 text-sm font-semibold text-green-400 shadow">
{complete ? "✓" : stepNumber}
</span>
)}
{drawLine ? (
<div className="h-full border-l border-slate-700"></div>
) : (
<div className="h-full"></div>
<div className="flex items-center gap-x-3">
<span className="flex h-6 w-6 items-center justify-center rounded border border-slate-700 bg-slate-800 py-1 text-xs font-semibold text-dimmed shadow">
{complete ? "✓" : stepNumber}
</span>
<Header2>{title}</Header2>
</div>
)}
</div>
);
@@ -0,0 +1,64 @@
"use client";
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "~/utils/cn";
const variations = {
large: {
container: "flex items-center gap-x-2 rounded-md hover:bg-slate-850 p-2",
root: "h-6 w-11",
thumb:
"h-5 w-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
text: "text-sm text-slate-400 group-hover:text-slate-200",
},
small: {
container:
"flex items-center gap-x-1.5 rounded hover:bg-slate-850 pr-1 py-[0.1rem] pl-1.5",
root: "h-3 w-6",
thumb:
"h-2.5 w-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
text: "text-xs text-slate-400 group-hover:text-slate-200 mt-0.5",
},
};
type SwitchProps = React.ComponentPropsWithoutRef<
typeof SwitchPrimitives.Root
> & {
label?: React.ReactNode;
variant: keyof typeof variations;
};
export const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
SwitchProps
>(({ className, variant, label, ...props }, ref) => {
const { container, root, thumb, text } = variations[variant];
return (
<SwitchPrimitives.Root
className={cn("group", container, className)}
{...props}
ref={ref}
>
{label ? (
<label className={cn("text", text)}>
{typeof label === "string" ? <span>{label}</span> : label}
</label>
) : null}
<div
className={cn(
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-background group-disabled:cursor-not-allowed group-disabled:opacity-50 group-data-[state=checked]:bg-primary group-data-[state=unchecked]:bg-slate-700 focus-visible:outline-none",
root
)}
>
<SwitchPrimitives.Thumb
className={cn(
thumb,
"pointer-events-none block rounded-full bg-slate-200 shadow-lg ring-0 transition-transform"
)}
/>
</div>
</SwitchPrimitives.Root>
);
});
+70 -48
View File
@@ -86,6 +86,7 @@ type TableCellBasicProps = {
className?: string;
alignment?: "left" | "center" | "right";
children: ReactNode;
colSpan?: number;
};
type TableHeaderCellProps = TableCellBasicProps & {
@@ -95,38 +96,11 @@ type TableHeaderCellProps = TableCellBasicProps & {
export const TableHeaderCell = forwardRef<
HTMLTableCellElement,
TableHeaderCellProps
>(({ className, alignment = "left", children, hiddenLabel = false }, ref) => {
let alignmentClassName = "text-left";
switch (alignment) {
case "center":
alignmentClassName = "text-center";
break;
case "right":
alignmentClassName = "text-right";
break;
}
return (
<th
ref={ref}
scope="col"
className={cn(
"px-4 py-3 align-middle text-xs font-semibold uppercase text-slate-400",
alignmentClassName,
className
)}
>
{hiddenLabel ? <span className="sr-only">{children}</span> : children}
</th>
);
});
type TableCellProps = TableCellBasicProps & {
to?: string;
};
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
({ className, alignment = "left", children, to }, ref) => {
>(
(
{ className, alignment = "left", children, colSpan, hiddenLabel = false },
ref
) => {
let alignmentClassName = "text-left";
switch (alignment) {
case "center":
@@ -137,30 +111,68 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
break;
}
return (
<th
ref={ref}
scope="col"
className={cn(
"px-4 py-3 align-middle text-xs font-semibold uppercase text-slate-400",
alignmentClassName,
className
)}
colSpan={colSpan}
>
{hiddenLabel ? <span className="sr-only">{children}</span> : children}
</th>
);
}
);
type TableCellProps = TableCellBasicProps & {
to?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
};
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
({ className, alignment = "left", children, colSpan, to, onClick }, ref) => {
let alignmentClassName = "text-left";
switch (alignment) {
case "center":
alignmentClassName = "text-center";
break;
case "right":
alignmentClassName = "text-right";
break;
}
const flexClasses = cn(
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-slate-400",
alignment === "left"
? "justify-start text-left"
: alignment === "center"
? "justify-center text-center"
: "justify-end text-right"
);
return (
<td
ref={ref}
className={cn(
"text-xs text-slate-400 transition group-hover:bg-slate-850/50",
to ? "cursor-pointer" : "px-4 py-3 align-middle",
!to && alignmentClassName,
to || onClick ? "cursor-pointer" : "px-4 py-3 align-middle",
!to && !onClick && alignmentClassName,
className
)}
colSpan={colSpan}
>
{to ? (
<Link
to={to}
className={cn(
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-slate-400",
alignment === "left"
? "justify-start text-left"
: alignment === "center"
? "justify-center text-center"
: "justify-end text-right"
)}
>
<Link to={to} className={flexClasses}>
{children}
</Link>
) : onClick ? (
<button onClick={onClick} className={flexClasses}>
{children}
</button>
) : (
<>{children}</>
)}
@@ -171,10 +183,20 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
export const TableCellChevron = forwardRef<
HTMLTableCellElement,
{ className?: string; to: string }
>(({ className, to }, ref) => {
{
className?: string;
to?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
>(({ className, to, onClick }, ref) => {
return (
<TableCell className={className} to={to} ref={ref} alignment="right">
<TableCell
className={className}
to={to}
onClick={onClick}
ref={ref}
alignment="right"
>
<ChevronRightIcon className="h-4 w-4 text-slate-700 transition group-hover:text-bright" />
</TableCell>
);
@@ -0,0 +1,16 @@
import { cn } from "~/utils/cn";
type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {};
export function TextArea({ className, rows, ...props }: TextAreaProps) {
return (
<textarea
{...props}
rows={rows ?? 6}
className={cn(
"w-full rounded-md border border-slate-800 bg-slate-850 px-3 text-sm text-bright ring-offset-background transition file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground hover:border-slate-750 hover:bg-slate-800 focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
/>
);
}
@@ -1,4 +1,4 @@
import { JobRunStatus } from ".prisma/client";
import type { JobRunStatus } from "@trigger.dev/database";
import {
CheckCircleIcon,
ClockIcon,
@@ -83,7 +83,7 @@ export function RunStatusIcon({
}
}
type RunBasicStatus =
export type RunBasicStatus =
| "WAITING"
| "PENDING"
| "RUNNING"
@@ -136,7 +136,7 @@ export function runStatusTitle(status: JobRunStatus): string {
}
}
function runStatusClassNameColor(status: JobRunStatus): string {
export function runStatusClassNameColor(status: JobRunStatus): string {
switch (status) {
case "SUCCESS":
return "text-green-500";
@@ -1,11 +1,11 @@
import { StopIcon } from "@heroicons/react/24/outline";
import { CheckIcon, ChevronRightIcon } from "@heroicons/react/24/solid";
import { CheckIcon } from "@heroicons/react/24/solid";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { RunList } from "~/presenters/RunListPresenter.server";
import { formatDateTime, formatDuration } from "~/utils";
import { runPath } from "~/utils/pathBuilder";
import { runDashboardPath } from "~/utils/pathBuilder";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
import { Callout } from "../primitives/Callout";
import { Spinner } from "../primitives/Spinner";
@@ -64,7 +64,7 @@ export function RunsTable({
</TableBlankRow>
) : (
runs.map((run) => {
const path = runPath(organization, project, job, run);
const path = runDashboardPath(organization, project, job, run);
return (
<TableRow key={run.id}>
<TableCell to={path}>#{run.number}</TableCell>
@@ -85,16 +85,22 @@ function ButtonList({ primary }: { primary: string }) {
</div>
<div className="flex flex-col items-start gap-2">
<Header3 className="mb-1 uppercase">Shortcut</Header3>
<Button variant="primary/small" shortcut="⌘S">
<Button
variant="primary/small"
shortcut={{ key: "s", modifiers: ["meta"] }}
>
Primary button
</Button>
<Button variant="secondary/small" shortcut="K">
<Button variant="secondary/small" shortcut={{ key: "f" }}>
Secondary button
</Button>
<Button variant="tertiary/small" shortcut="I">
<Button variant="tertiary/small" shortcut={{ key: "i" }}>
Tertiary button
</Button>
<Button variant="danger/small" shortcut="⌘S">
<Button
variant="danger/small"
shortcut={{ key: "s", modifiers: ["meta"] }}
>
Danger button
</Button>
</div>
@@ -195,16 +201,28 @@ function ButtonList({ primary }: { primary: string }) {
</div>
<div className="flex flex-col items-start gap-2">
<Header3 className="mb-1 uppercase">Shortcut</Header3>
<Button variant="primary/medium" shortcut="⌘S">
<Button
variant="primary/medium"
shortcut={{ key: "s", modifiers: ["meta"] }}
>
Primary button
</Button>
<Button variant="secondary/medium" shortcut="F">
<Button
variant="secondary/medium"
shortcut={{ key: "s", modifiers: ["meta"] }}
>
Secondary button
</Button>
<Button variant="tertiary/medium" shortcut="⌘S">
<Button
variant="tertiary/medium"
shortcut={{ key: "s", modifiers: ["meta"] }}
>
Tertiary button
</Button>
<Button variant="danger/medium" shortcut="⌘S">
<Button
variant="danger/medium"
shortcut={{ key: "s", modifiers: ["meta"] }}
>
Danger button
</Button>
</div>
@@ -55,7 +55,7 @@ function BasicCheckBox() {
id="check2"
variant="button"
label="This is a button checkbox with a badge"
badge="This is a badge"
badges={["This is a badge"]}
disabled={isDisabled}
/>
<Checkbox
@@ -71,7 +71,7 @@ function BasicCheckBox() {
name="Description checkbox"
id="check3"
variant="description"
badge="This is a badge"
badges={["This is a badge"]}
label="This is a checkbox with a description and badge"
description="This is a long checkbox description that goes full width. Grants full access to public and private repositories including read and write access to code, commit statuses, repository invitations, collaborators, deployment statuses, and repository webhooks. Note: In addition to repository related resources, the repo scope also grants access to manage organization-owned resources including projects, invitations, team memberships and webhooks. This scope also grants the ability to manage projects owned by users."
disabled={isDisabled}
@@ -0,0 +1,108 @@
import type { Meta, StoryObj } from "@storybook/react";
import { withDesign } from "storybook-addon-designs";
import { ClipboardField } from "../primitives/ClipboardField";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
const meta: Meta = {
title: "Primitives/ClipboardField",
decorators: [withDesign],
};
export default meta;
type Story = StoryObj<typeof ClipboardFieldExample>;
export const Basic: Story = {
render: () => <ClipboardFieldExample />,
};
function ClipboardFieldExample() {
return (
<div className="flex gap-8">
<div className="flex flex-col items-start gap-y-8 p-8">
<ClipboardField value="copy paste me" variant="primary/small" />
<ClipboardField value="copy paste me" variant="secondary/small" />
<ClipboardField value="copy paste me" variant="tertiary/small" />
<ClipboardField
value="with leadingIcon"
variant="tertiary/small"
icon={<EnvironmentLabel environment={{ type: "PRODUCTION" }} />}
/>
<ClipboardField
value="with leadingIcon"
variant="tertiary/small"
icon="search"
/>
<ClipboardField value="copy paste me" variant="primary/medium" />
<ClipboardField value="copy paste me" variant="secondary/medium" />
<ClipboardField value="copy paste me" variant="tertiary/medium" />
<ClipboardField
value="with leadingIcon"
variant="tertiary/medium"
icon={<EnvironmentLabel environment={{ type: "DEVELOPMENT" }} />}
/>
<ClipboardField
value="with leadingIcon"
variant="tertiary/medium"
icon="search"
/>
</div>
<div className="flex flex-col items-start gap-y-8 p-8">
<ClipboardField
value="copy paste me"
variant="primary/small"
secure={true}
/>
<ClipboardField
value="copy paste me"
variant="secondary/small"
secure={true}
/>
<ClipboardField
value="copy paste me"
variant="tertiary/small"
secure={true}
/>
<ClipboardField
value="with leadingIcon"
variant="tertiary/small"
icon={<EnvironmentLabel environment={{ type: "STAGING" }} />}
secure={true}
/>
<ClipboardField
value="with leadingIcon"
variant="tertiary/small"
icon="search"
secure={true}
/>
<ClipboardField
value="copy paste me"
variant="primary/medium"
secure={true}
/>
<ClipboardField
value="copy paste me"
variant="secondary/medium"
secure={true}
/>
<ClipboardField
value="copy paste me"
variant="tertiary/medium"
secure={true}
/>
<ClipboardField
value="with leadingIcon"
variant="tertiary/medium"
icon={<EnvironmentLabel environment={{ type: "PRODUCTION" }} />}
secure={true}
/>
<ClipboardField
value="with leadingIcon"
variant="tertiary/medium"
icon="search"
secure={true}
/>
</div>
</div>
);
}
@@ -128,7 +128,7 @@ function LoginForm() {
Continue with Email
</Button>
<Paragraph variant="small" className="mt-2 text-center">
By connecting your GitHub account you agree to our{" "}
By creating an account you agree to our{" "}
<TextLink href="#">terms</TextLink> and{" "}
<TextLink href="#">privacy</TextLink> policies.
</Paragraph>
@@ -42,7 +42,11 @@ function PageHeaders() {
<PageTitleRow>
<PageTitle title="Organizations" />
<PageButtons>
<LinkButton to={""} variant="primary/small" shortcut="N">
<LinkButton
to={""}
variant="primary/small"
shortcut={{ key: "n" }}
>
Create a new Organization
</LinkButton>
</PageButtons>
@@ -61,7 +65,11 @@ function PageHeaders() {
backButton={{ to: "#", text: "Orgs" }}
/>
<PageButtons>
<LinkButton to={""} variant="primary/small" shortcut="N">
<LinkButton
to={""}
variant="primary/small"
shortcut={{ key: "n" }}
>
Create a new Organization
</LinkButton>
</PageButtons>
@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from "@storybook/react";
import { withDesign } from "storybook-addon-designs";
import { useState } from "react";
import { Button } from "../primitives/Buttons";
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
const meta: Meta = {
title: "Primitives/RadioGroup",
decorators: [withDesign],
};
export default meta;
type Story = StoryObj<typeof RadioGroupExample>;
export const Basic: Story = {
render: () => <RadioGroupExample />,
};
function RadioGroupExample() {
const [isDisabled, setIsDisabled] = useState(false);
return (
<div className="flex flex-col items-start gap-y-8 p-8">
<Button
onClick={() => setIsDisabled((d) => !d)}
variant="primary/medium"
className="max-w-fit"
>
{isDisabled ? "Enable" : "Disable"}
</Button>
<form>
<RadioGroup name="simple" disabled={isDisabled} className="grid gap-2">
<RadioGroupItem
id="r2"
label="Simple small"
value={"1"}
variant="simple/small"
/>
<RadioGroupItem id="r3" label="Simple" value={"2"} variant="simple" />
<RadioGroupItem
id="r4"
label="Button small"
value={"3"}
variant="button/small"
/>
<RadioGroupItem id="r5" label="Button" value={"4"} variant="button" />
<RadioGroupItem
id="r6"
label="This is a label"
description="This is a description"
value={"5"}
variant="description"
/>
</RadioGroup>
</form>
</div>
);
}
@@ -0,0 +1,74 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Switch } from "../primitives/Switch";
import { ShortcutKey } from "../primitives/ShortcutKey";
import { ShortcutDefinition } from "~/hooks/useShortcutKeys";
import { Button } from "../primitives/Buttons";
import { OperatingSystemContextProvider } from "../primitives/OperatingSystemProvider";
import { Header1 } from "../primitives/Headers";
const meta: Meta = {
title: "Primitives/ShortcutKey",
};
export default meta;
type Story = StoryObj<typeof Collection>;
export const ShortcutKeys: Story = {
render: () => <Collection />,
};
const shortcuts: ShortcutDefinition[] = [
{ key: "esc" },
{ key: "f" },
{ key: "f", modifiers: ["meta"] },
{ key: "k", modifiers: ["meta"] },
{ key: "del", modifiers: ["ctrl", "alt"] },
];
function Collection() {
return (
<div className="flex flex-col items-start gap-y-4">
<Set platform="mac" />
<Set platform="windows" />
</div>
);
}
function Set({ platform }: { platform: "mac" | "windows" }) {
return (
<OperatingSystemContextProvider platform={platform}>
<Header1>{platform}</Header1>
{shortcuts.map((shortcut, index) => (
<div key={index} className="flex items-center gap-x-2">
<ShortcutKey shortcut={shortcut} variant="small" />
<ShortcutKey shortcut={shortcut} variant="medium" />
<Button variant="primary/small" shortcut={shortcut}>
Primary small
</Button>
<Button variant="secondary/small" shortcut={shortcut}>
Secondary small
</Button>
<Button variant="tertiary/small" shortcut={shortcut}>
Tertiary small
</Button>
<Button variant="danger/small" shortcut={shortcut}>
Danger small
</Button>
<Button variant="primary/medium" shortcut={shortcut}>
Primary medium
</Button>
<Button variant="secondary/medium" shortcut={shortcut}>
Secondary medium
</Button>
<Button variant="tertiary/medium" shortcut={shortcut}>
Tertiary medium
</Button>
<Button variant="danger/medium" shortcut={shortcut}>
Danger medium
</Button>
</div>
))}
</OperatingSystemContextProvider>
);
}
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Switch } from "../primitives/Switch";
const meta: Meta = {
title: "Primitives/Switch",
};
export default meta;
type Story = StoryObj<typeof Collection>;
export const Switches: Story = {
render: () => <Collection />,
};
function Collection() {
return (
<div className="flex flex-col items-start gap-y-4 p-4">
<Switch variant="large" />
<Switch variant="large" disabled />
<Switch variant="large" label="Toggle me" />
<Switch variant="large" label="Toggle me" disabled />
<Switch variant="small" />
<Switch variant="small" disabled />
<Switch variant="small" label="Toggle me" />
<Switch variant="small" label="Toggle me" disabled />
</div>
);
}
+3 -3
View File
@@ -1,10 +1,10 @@
import { PrismaClient, Prisma } from ".prisma/client";
import { PrismaClient, Prisma } from "@trigger.dev/database";
import invariant from "tiny-invariant";
import { z } from "zod";
export type PrismaTransactionClient = Omit<
PrismaClient,
"$connect" | "$disconnect" | "$on" | "$transaction" | "$use"
"$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends"
>;
export type PrismaClientOrTransaction = PrismaClient | PrismaTransactionClient;
@@ -96,7 +96,7 @@ function getClient() {
}
export { prisma };
export type { PrismaClient } from ".prisma/client";
export type { PrismaClient } from "@trigger.dev/database";
export const PrismaErrorSchema = z.object({
code: z.string(),
+8 -26
View File
@@ -1,35 +1,17 @@
import { RemixBrowser, useLocation, useMatches } from "@remix-run/react";
import { hydrateRoot } from "react-dom/client";
import * as Sentry from "@sentry/remix";
import { useEffect } from "react";
import posthog from "posthog-js";
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
import { OperatingSystemContextProvider } from "./components/primitives/OperatingSystemProvider";
hydrateRoot(
document,
<LocaleContextProvider locales={window.navigator.languages as string[]}>
<RemixBrowser />
</LocaleContextProvider>
<OperatingSystemContextProvider
platform={window.navigator.userAgent.includes("Mac") ? "mac" : "windows"}
>
<LocaleContextProvider locales={window.navigator.languages as string[]}>
<RemixBrowser />
</LocaleContextProvider>
</OperatingSystemContextProvider>
);
if (process.env.NODE_ENV === "production") {
Sentry.init({
dsn: "https://bf96820b08004fa4b2e1506f2ac74a14@o4504419574087680.ingest.sentry.io/4504419607052288",
tracesSampleRate: 1,
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.remixRouterInstrumentation(
useEffect,
useLocation,
useMatches
),
}),
//casted because TypeScript is unhappy about the type from PostHog
new posthog.SentryIntegration(
posthog,
"triggerdev",
4504419607052288
) as any,
],
});
}
+37 -20
View File
@@ -1,4 +1,3 @@
import * as Sentry from "~/services/sentry.server";
import * as Worker from "~/services/worker.server";
import { PassThrough } from "stream";
import { renderToPipeableStream } from "react-dom/server";
@@ -8,6 +7,10 @@ import type { EntryContext, Headers } from "@remix-run/node"; // or cloudflare/d
import { parseAcceptLanguage } from "intl-parse-accept-language";
import isbot from "isbot";
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
import {
OperatingSystemContextProvider,
OperatingSystemPlatform,
} from "./components/primitives/OperatingSystemProvider";
const ABORT_DELAY = 30000;
@@ -22,6 +25,13 @@ export default function handleRequest(
validate: Intl.DateTimeFormat.supportedLocalesOf,
});
//get whether it's a mac or pc from the headers
const platform: OperatingSystemPlatform = request.headers
.get("user-agent")
?.includes("Mac")
? "mac"
: "windows";
// If the request is from a bot, we want to wait for the full
// response to render before sending it to the client. This
// ensures that bots can see the full page content.
@@ -31,7 +41,8 @@ export default function handleRequest(
responseStatusCode,
responseHeaders,
remixContext,
locales
locales,
platform
);
}
@@ -40,7 +51,8 @@ export default function handleRequest(
responseStatusCode,
responseHeaders,
remixContext,
locales
locales,
platform
);
}
@@ -49,17 +61,20 @@ function serveTheBots(
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
locales: string[]
locales: string[],
platform: OperatingSystemPlatform
) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<LocaleContextProvider locales={locales}>
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</LocaleContextProvider>,
<OperatingSystemContextProvider platform={platform}>
<LocaleContextProvider locales={locales}>
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</LocaleContextProvider>
</OperatingSystemContextProvider>,
{
// Use onAllReady to wait for the entire document to be ready
onAllReady() {
@@ -87,18 +102,21 @@ function serveBrowsers(
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
locales: string[]
locales: string[],
platform: OperatingSystemPlatform
) {
return new Promise((resolve, reject) => {
let didError = false;
const { pipe, abort } = renderToPipeableStream(
<LocaleContextProvider locales={locales}>
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</LocaleContextProvider>,
<OperatingSystemContextProvider platform={platform}>
<LocaleContextProvider locales={locales}>
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</LocaleContextProvider>
</OperatingSystemContextProvider>,
{
// use onShellReady to wait until a suspense boundary is triggered
onShellReady() {
@@ -125,5 +143,4 @@ function serveBrowsers(
});
}
Sentry.init();
Worker.init().catch(console.error);
+3 -3
View File
@@ -17,7 +17,6 @@ const EnvironmentSchema = z.object({
.string()
// eslint-disable-next-line turbo/no-undeclared-env-vars
.default(process.env.FC_URL ?? "https://app.trigger.dev"),
SENTRY_DSN: z.string().optional(),
APP_ENV: z
.union([
z.literal("development"),
@@ -29,12 +28,13 @@ const EnvironmentSchema = z.object({
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
POSTHOG_PROJECT_KEY: z.string().optional(),
MAGIC_LINK_SECRET: z.string(),
GITHUB_CLIENT_ID: z.string(),
GITHUB_SECRET: z.string(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
FROM_EMAIL: z.string(),
REPLY_TO_EMAIL: z.string(),
RESEND_API_KEY: z.string(),
SESSION_SECRET: z.string(),
PLAIN_API_KEY: z.string().optional(),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
+18
View File
@@ -0,0 +1,18 @@
import { requestUrl } from "./utils";
export type TriggerFeatures = {
isManagedCloud: boolean;
};
// If the request host is cloud.trigger.dev then we are on the managed cloud
// or if env.NODE_ENV is development
export function featuresForRequest(request: Request): TriggerFeatures {
const url = requestUrl(request);
const isManagedCloud =
url.host === "cloud.trigger.dev" || process.env.NODE_ENV === "development";
return {
isManagedCloud,
};
}
+12 -35
View File
@@ -1,53 +1,30 @@
import { DEV_ENVIRONMENT, LIVE_ENVIRONMENT } from "~/consts";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import { useMatchesData } from "~/utils";
import { MatchedProject, useOptionalProject } from "./useProject";
function isRuntimeEnvironment(
environment: any
): environment is RuntimeEnvironment {
return (
environment &&
typeof environment === "object" &&
typeof environment.slug === "string"
);
}
export type ProjectJobEnvironment = MatchedProject["environments"][number];
function isRuntimeEnvironments(
environments: any
): environments is RuntimeEnvironment[] {
return (
environments &&
typeof environments === "object" &&
Array.isArray(environments) &&
environments.every(isRuntimeEnvironment)
);
}
export function useEnvironments() {
const project = useOptionalProject();
export function useEnvironments(): RuntimeEnvironment[] | undefined {
const routeMatch = useMatchesData("routes/_app.orgs.$organizationSlug");
if (
!routeMatch ||
!isRuntimeEnvironments(routeMatch.data.organization.environments)
) {
if (!project) {
return undefined;
}
return routeMatch.data.organization.environments;
return project.environments;
}
export function useDevEnvironment(): RuntimeEnvironment | undefined {
export function useDevEnvironment() {
const environments = useEnvironments();
if (!environments) return;
return environments.find(
(environment: any) => environment.slug === DEV_ENVIRONMENT
);
return environments.find((environment) => environment.type === "DEVELOPMENT");
}
export function useLiveEnvironment(): RuntimeEnvironment | undefined {
export function useProdEnvironment() {
const environments = useEnvironments();
if (!environments) return;
return environments.find(
(environment: any) => environment.slug === LIVE_ENVIRONMENT
);
return environments.find((environment) => environment.type === "PRODUCTION");
}
+9
View File
@@ -0,0 +1,9 @@
import { useTypedRouteLoaderData } from "remix-typedjson";
import { loader } from "../root";
import type { TriggerFeatures } from "~/features.server";
export function useFeatures(): TriggerFeatures {
const routeMatch = useTypedRouteLoaderData<typeof loader>("root");
return routeMatch?.features ?? { isManagedCloud: false };
}
+32
View File
@@ -0,0 +1,32 @@
import { ProjectJob } from "./useProject";
import { useTextFilter } from "./useTextFilter";
export function useFilterJobs(jobs: ProjectJob[]) {
const { filterText, setFilterText, filteredItems } =
useTextFilter<ProjectJob>({
items: jobs,
filter: (job, text) => {
if (job.slug.toLowerCase().includes(text.toLowerCase())) return true;
if (job.title.toLowerCase().includes(text.toLowerCase())) return true;
if (job.event.title.toLowerCase().includes(text.toLowerCase()))
return true;
if (
job.integrations.some((integration) =>
integration.title.toLowerCase().includes(text.toLowerCase())
)
)
return true;
if (
job.properties &&
job.properties.some((property) =>
property.text.toLowerCase().includes(text.toLowerCase())
)
)
return true;
return false;
},
});
return { filterText, setFilterText, filteredItems };
}
@@ -0,0 +1,22 @@
import {
UseDataFunctionReturn,
useTypedRouteLoaderData,
} from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route";
export type MatchedClient = UseDataFunctionReturn<typeof loader>["client"];
export function useOptionalIntegrationClient() {
const routeMatch = useTypedRouteLoaderData<typeof loader>(
"routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam"
);
return routeMatch?.client;
}
export function useIntegrationClient() {
const client = useOptionalIntegrationClient();
invariant(client, "Client must be defined");
return client;
}
+11 -16
View File
@@ -1,34 +1,29 @@
import type { UseDataFunctionReturn } from "remix-typedjson";
import type { loader as jobLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
import { hydrateObject, useMatchesData } from "~/utils";
import { useOptionalProject } from "./useProject";
import {
UseDataFunctionReturn,
useTypedRouteLoaderData,
} from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
import { useOptionalProject } from "./useProject";
export type MatchedJob = UseDataFunctionReturn<typeof jobLoader>["job"];
export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
export function useOptionalJob() {
const project = useOptionalProject();
const routeMatch = useMatchesData(
const routeMatch = useTypedRouteLoaderData<typeof loader>(
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam"
);
if (!project || !routeMatch || !routeMatch.data.job) {
if (!project || !routeMatch || !routeMatch.job) {
return undefined;
}
if (routeMatch.data.job == null) {
if (routeMatch.job == null) {
return undefined;
}
const result = hydrateObject<UseDataFunctionReturn<typeof jobLoader>["job"]>(
routeMatch.data.job
);
if (result == null) return undefined;
//get the job from the list on the project
const job = project.jobs.find((j) => j.id === result.id);
return job;
return project.jobs.find((j) => j.id === routeMatch.job.id);
}
export function useJob() {
+13 -15
View File
@@ -1,8 +1,11 @@
import type { UseDataFunctionReturn } from "remix-typedjson";
import type { loader as appLoader } from "~/routes/_app/route";
import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route";
import { hydrateObject, useMatchesData } from "~/utils";
import {
UseDataFunctionReturn,
useTypedRouteLoaderData,
} from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route";
import type { loader as appLoader } from "~/routes/_app/route";
import { hydrateObject, useMatchesData } from "~/utils";
export type MatchedOrganization = UseDataFunctionReturn<
typeof appLoader
@@ -20,24 +23,19 @@ export function useOrganizations() {
export function useOptionalOrganization() {
const orgs = useOptionalOrganizations();
const routeMatch = useMatchesData("routes/_app.orgs.$organizationSlug");
const routeMatch = useTypedRouteLoaderData<typeof orgLoader>(
"routes/_app.orgs.$organizationSlug"
);
if (!orgs || !routeMatch || !routeMatch.data.organization) {
if (!orgs || !routeMatch || !routeMatch.organization) {
return undefined;
}
if (routeMatch.data.organization == null) {
if (routeMatch.organization === null) {
return undefined;
}
const result = hydrateObject<
UseDataFunctionReturn<typeof orgLoader>["organization"]
>(routeMatch.data.organization);
if (result == null) return undefined;
const org = orgs.find((o) => o.id === result.id);
return org;
return orgs.find((o) => o.id === routeMatch.organization.id);
}
export function useOrganization() {
+1
View File
@@ -14,6 +14,7 @@ export const usePostHog = (apiKey?: string, logging = false): void => {
//start PostHog once
useEffect(() => {
if (apiKey === undefined) return;
if (apiKey === "") return;
if (postHogInitialized.current === true) return;
if (logging) console.log("posthog.init", apiKey);
postHogInitialized.current = true;
+8 -17
View File
@@ -1,29 +1,20 @@
import type { UseDataFunctionReturn } from "remix-typedjson";
import {
UseDataFunctionReturn,
useTypedRouteLoaderData,
} from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader as projectLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
import { hydrateObject, useMatchesData } from "~/utils";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
export type MatchedProject = UseDataFunctionReturn<
typeof projectLoader
>["project"];
export type MatchedProject = UseDataFunctionReturn<typeof loader>["project"];
export type ProjectJob = MatchedProject["jobs"][number];
export function useOptionalProject() {
const routeMatch = useMatchesData(
const routeMatch = useTypedRouteLoaderData<typeof loader>(
"routes/_app.orgs.$organizationSlug.projects.$projectParam"
);
if (!routeMatch || !routeMatch.data.project) {
return undefined;
}
const result = hydrateObject<
UseDataFunctionReturn<typeof projectLoader>["project"]
>(routeMatch.data.project);
if (result == null) return undefined;
return result;
return routeMatch?.project;
}
export function useProject() {
+10 -14
View File
@@ -1,32 +1,28 @@
import type { UseDataFunctionReturn } from "remix-typedjson";
import type { loader as runLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route";
import { hydrateObject, useMatchesData } from "~/utils";
import { useOptionalProject } from "./useProject";
import {
UseDataFunctionReturn,
useTypedRouteLoaderData,
} from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader as runLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route";
import { useOptionalProject } from "./useProject";
export type MatchedRun = UseDataFunctionReturn<typeof runLoader>["run"];
export function useOptionalRun() {
const project = useOptionalProject();
const routeMatch = useMatchesData(
const routeMatch = useTypedRouteLoaderData<typeof runLoader>(
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam"
);
if (!project || !routeMatch || !routeMatch.data.run) {
if (!project || !routeMatch || !routeMatch.run) {
return undefined;
}
if (routeMatch.data.run == null) {
if (routeMatch.run == null) {
return undefined;
}
const run = hydrateObject<UseDataFunctionReturn<typeof runLoader>["run"]>(
routeMatch.data.run
);
if (run == null) return undefined;
return run;
return routeMatch.run;
}
export function useRun() {
+43
View File
@@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider";
export type Modifier = "alt" | "ctrl" | "meta" | "shift";
export type Shortcut = {
key: string;
modifiers?: Modifier[];
};
export type ShortcutDefinition =
| {
windows: Shortcut;
mac: Shortcut;
}
| Shortcut;
type useShortcutKeysProps = {
shortcut: ShortcutDefinition;
action: (event: KeyboardEvent) => void;
disabled?: boolean;
};
export function useShortcutKeys({
shortcut,
action,
disabled = false,
}: useShortcutKeysProps) {
const keys = createKeysFromShortcut(shortcut);
useHotkeys(keys, action, { enabled: !disabled });
}
function createKeysFromShortcut(shortcut: ShortcutDefinition) {
const { platform } = useOperatingSystem();
const isMac = platform === "mac";
let relevantShortcut =
"mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
const modifiers = relevantShortcut.modifiers;
const character = relevantShortcut.key;
return modifiers ? modifiers.map((k) => k).join("+") + "+" : "" + character;
}
@@ -1,5 +0,0 @@
export type {
ApiConnectionType,
ApiConnectionClient,
ApiConnection,
} from ".prisma/client";

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