Compare commits

...

145 Commits

Author SHA1 Message Date
Eric Allam 79eab3d62f Cleanup unused graphile tasks 2023-07-07 11:46:56 +01:00
Eric Allam 6527723a0e Refactor index endpoint to work sequentially so we can add associations between sources and jobs 2023-07-07 11:36:01 +01:00
Eric Allam 3d6652f132 WIP plain integration 2023-07-07 10:59:36 +01:00
Matt Aitken da5d9779d4 Check if the Highlight id is defined before attempting to send the error 2023-07-06 21:03:20 +01:00
Matt Aitken 865399e753 Enabled tracing so we can track down errors between frontend and backend 2023-07-06 20:56:50 +01:00
Matt Aitken 960ea6c8dd Removed the throw button from the NavBar… obvs should never have been committed ;) 2023-07-06 20:56:35 +01:00
Matt Aitken 39ab48f0cb Highlight server-side error tracking 2023-07-06 20:45:20 +01:00
Matt Aitken 7341b4cf61 Highlight identifies users 2023-07-06 20:45:00 +01:00
Matt Aitken 28c5acc6af Added Highlight to the project, with basic client-side tracking 2023-07-06 19:34:53 +01:00
Matt Aitken 4366eefc66 Removed unused import 2023-07-06 19:34:05 +01:00
Matt Aitken cb95e761c1 Added an error boundary to the _app route 2023-07-06 19:33:32 +01:00
Matt Aitken 7136748463 Added an error boundary to org 2023-07-06 19:29:42 +01:00
Matt Aitken b0cacfd75c Added an error boundary to project 2023-07-06 19:29:32 +01:00
Matt Aitken 81f03dac79 Better error when a job isn’t found 2023-07-06 19:29:17 +01:00
Matt Aitken e37d1eadcd Breadcrumb null checks in case error boundaries are hit deep in the app 2023-07-06 19:29:00 +01:00
Matt Aitken 23fb310374 Reusable ErrorDisplay 2023-07-06 19:28:36 +01:00
Matt Aitken 797a1732d2 Organize imports on ProjectSideMenu 2023-07-06 19:08:06 +01:00
Matt Aitken c405a417e9 If an error boundary is hit the meta function can return undefined… 2023-07-06 19:07:46 +01:00
James Ritchie 808deb1d77 Added data attributes to the side menu items 2023-07-06 17:50:44 +01:00
Eric Allam 6ac1473ec3 Merge pull request #139 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-06 15:31:53 +01:00
github-actions[bot] 9c183abf4b chore: Update version for release (next) 2023-07-06 14:27:36 +00:00
Eric Allam 33c33faae9 Adding the typeform integration to the catalog 2023-07-06 15:26:02 +01:00
Matt Aitken 4cccbc2039 PostHog updated to change hooks, also added “job” group 2023-07-06 14:58:29 +01:00
Matt Aitken cc061dedf8 Change hooks for User, Org, Project, Job 2023-07-06 14:58:29 +01:00
Matt Aitken 402ce2210f New hook that lets you trigger an action when an object changes 2023-07-06 14:58:29 +01:00
Matt Aitken 329c5c4070 Updated posthogjs 2023-07-06 14:58:29 +01:00
Matt Aitken 878aeb7a8c useOrganizationChanged 2023-07-06 14:58:29 +01:00
Eric Allam 3ee396d788 Adding the typeform integration with webhook support 2023-07-06 13:42:49 +01:00
Eric Allam 090037602c WIP create-integration CLI command 2023-07-06 13:42:49 +01:00
Matt Aitken bee77051e7 Added UI to guide you to fixing unconfigured integrations 2023-07-06 11:56:19 +01:00
Matt Aitken be6741a4a8 Example Job that has an unconfigured Integration (for testing) 2023-07-06 11:56:19 +01:00
Matt Aitken 3e9b440322 NamedIcon uses the new type 2023-07-06 11:56:19 +01:00
Matt Aitken dfea5bc77c The connection-alert trigger type has an amber icon so it doesn’t look like an error 2023-07-06 11:56:19 +01:00
Matt Aitken 1eef30e298 Callout uses to and supports internal and external links 2023-07-06 11:56:19 +01:00
James Ritchie e5b9a9de3f Noun’d up Job 2023-07-06 11:36:13 +01:00
Matt Aitken dfedd3a6e8 Consistently use “Log in” everywhere 2023-07-06 10:08:09 +01:00
Matt Aitken a19845b4ca Added a jsdoc markr to the source id 2023-07-06 09:32:15 +01:00
Matt Aitken 094f6f5a7a jsdocs for DynamicTrigger and DynamicSchedule changeset 2023-07-06 00:37:01 +01:00
Matt Aitken fa60edce2b jsdocs for DynamicTrigger and DynamicSchedule 2023-07-06 00:36:08 +01:00
Matt Aitken ba44652457 intervalTrigger() jsdocs 2023-07-05 20:17:09 +01:00
Matt Aitken 931be39912 cronTrigger jsdocs 2023-07-05 19:32:35 +01:00
James Ritchie 5048a27acb Capital J for Job 2023-07-05 18:27:04 +01:00
Matt Aitken 71fc615b31 Fix for the profile photo sizing being weird 2023-07-05 18:05:41 +01:00
Eric Allam 20ed4e22ef Implement the cloud private beta invite form
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-07-05 17:27:59 +01:00
Matt Aitken fc2d337a39 Optional trailing icon on a <TextLink> 2023-07-05 17:22:53 +01:00
Matt Aitken 31d96f3862 External icon uses the small version 2023-07-05 17:22:44 +01:00
Matt Aitken 7b43853ec6 Help content for the Job page 2023-07-05 17:11:45 +01:00
Matt Aitken 40fa913272 TextLink can have a to or an href 2023-07-05 17:10:34 +01:00
Matt Aitken cdd5482aad Capitalized Runs 2023-07-05 17:10:17 +01:00
Matt Aitken 4a3fa20ee7 Export the callout styles 2023-07-05 17:10:10 +01:00
Matt Aitken d49ed4cd47 Fix for div inside paragraph error 2023-07-05 17:09:53 +01:00
James Ritchie db3cf77f5b Updated the pagination buttons style 2023-07-05 17:08:35 +01:00
James Ritchie c4a08961d6 Added a Help Panel to the Jobs list page 2023-07-05 17:08:35 +01:00
James Ritchie 5ba1643d46 Added a new qr code named icon 2023-07-05 16:57:33 +01:00
Eric Allam d189f425e8 Limit access to the cloud product to people who have invite codes 2023-07-05 16:31:41 +01:00
Matt Aitken 0fe6fb83ac The test help panel is open when you load the page 2023-07-05 16:09:20 +01:00
Matt Aitken f15ae41f48 Help lightbulb is always yellow 2023-07-05 16:09:20 +01:00
Eric Allam 46a07fc72e Merge pull request #138 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-05 15:59:30 +01:00
github-actions[bot] 83af317739 chore: Update version for release (next) 2023-07-05 14:57:17 +00:00
Eric Allam f4db1b2735 @trigger.dev/cli init - Create a better file structure 2023-07-05 15:54:58 +01:00
Eric Allam 6d4922f419 api.trigger.dev is now the default cloud url 2023-07-05 14:47:41 +01:00
Eric Allam 6af098ee8a Default the apiUrl to cloud.trigger.dev 2023-07-05 12:29:48 +01:00
Eric Allam 1dcb2ebcac Update pnpm lock file 2023-07-05 12:25:10 +01:00
Eric Allam f65cb463cc Merge pull request #137 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-05 12:20:43 +01:00
github-actions[bot] 2b4ce97f8a chore: Update version for release (next) 2023-07-05 11:12:37 +00:00
Eric Allam 25abf319ab PageInfoProperty not puts the value inside a paragraph even if it’s a react node
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-07-05 12:11:12 +01:00
James Ritchie 77807ab42a Changed ‘Clients’ to ‘Endpoints’ on the API keys page and reordered URL in table 2023-07-05 11:38:06 +01:00
Eric Allam f21990b14f Removed the datetime logging
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-07-05 11:36:44 +01:00
Eric Allam 1873d10e95 Normalize the whitespace characters in formatted date times 2023-07-05 11:13:26 +01:00
Matt Aitken 32ff3f3136 @trigger.dev/github: Fixed some properties that had API urls instead of HTML urls 2023-07-05 11:12:27 +01:00
Matt Aitken 354b0aefb0 “This Run returned nothing.” 2023-07-05 10:59:37 +01:00
Matt Aitken aaa70a9aab eventTrigger() jsdocs 2023-07-05 10:59:37 +01:00
Eric Allam 3b90739087 Rework the DateTime component to use a Fragment and render the correct datetime timezone in a useEffect 2023-07-05 10:51:08 +01:00
Matt Aitken acaae993a5 Run context jsdocs 2023-07-04 22:55:49 +01:00
Eric Allam a74f0322e2 Attempt to fix the prisma “Transaction already closed: Could not perform operation” error
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
- Removes the transaction stuff from StartRunService (can try putting it back more granularly)
- Uses $transaction in TestJobService
- Better queue names for the startRun and deliverEvent worker job
2023-07-04 22:11:53 +01:00
Matt Aitken 1a0286f6b0 Run pnpm i 2023-07-04 18:52:54 +01:00
Matt Aitken 64182483e2 Merge pull request #136 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-04 18:37:46 +01:00
github-actions[bot] 87a25fba62 chore: Update version for release (next) 2023-07-04 17:34:44 +00:00
Matt Aitken 12803536fa OpenAI integration: listModels returns the response.data instead of response.data.data 2023-07-04 18:33:27 +01:00
Matt Aitken f160b34b34 isTriggerError jsdocs 2023-07-04 16:28:58 +01:00
Matt Aitken cca7da9da2 Better docs for io.try 2023-07-04 16:15:47 +01:00
Matt Aitken 01cf5f3bdc io.try jsdocs 2023-07-04 16:14:31 +01:00
Matt Aitken 33a928a9ea Removed new job examples from general 2023-07-04 15:30:56 +01:00
Matt Aitken c83443a41a io.runTask and io.registerTrigger jsdocs 2023-07-04 15:30:06 +01:00
Eric Allam 374007bffd Fixed incorrectly scoping job test runs to the specific selected project
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-07-04 15:23:19 +01:00
Eric Allam 092382f319 Cleanup some of the date times 2023-07-04 15:14:45 +01:00
Eric Allam 79a0532e1d Handle OS signals in the docker container
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
2023-07-04 14:36:59 +01:00
Eric Allam 56c7c8b1ee Better handle interactive transaction errors and adding increased timeouts 2023-07-04 14:36:59 +01:00
Matt Aitken 722fe7b71e registerCron and unregisterCron jsdocs 2023-07-04 12:48:32 +01:00
Eric Allam 759793b2e3 Fix hydration errors by using the LocaleProvider for all rendered dates, and always use UTC to prevent timezone mismatches 2023-07-04 12:36:42 +01:00
Matt Aitken 99c6cd0371 io.registerInterval and io.unregisterInterval jsdocs 2023-07-04 11:22:28 +01:00
Eric Allam 9fbc5ab8de Adding logging to investigate hydration errors 2023-07-04 11:09:47 +01:00
Eric Allam 75ac2c9961 Scheduled triggers are now disabled on dev env by default
Also added pk api key to the environments now
2023-07-04 10:59:05 +01:00
Eric Allam 87330dd863 Merge pull request #135 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-04 10:50:18 +01:00
github-actions[bot] 38fe9302cf chore: Update version for release (next) 2023-07-04 09:36:25 +00:00
Eric Allam 33a455c593 pnpm i 2023-07-04 10:34:29 +01:00
Eric Allam 37db0ac58d Remove @trigger.dev/internal changeset 2023-07-04 10:33:00 +01:00
Eric Allam 2cbf50b1cc @trigger.dev/sdk: deliverAt and timestamp event properties are now dates 2023-07-04 10:32:08 +01:00
Matt Aitken e26923eb19 backgroundFetch jsdocs 2023-07-04 10:31:44 +01:00
Eric Allam 3e63bcfb11 Merge pull request #134 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-03 22:50:55 +01:00
github-actions[bot] 1e1c18c4a1 chore: Update version for release (next) 2023-07-03 21:49:48 +00:00
Eric Allam 953e7fc918 Allow scheduled jobs to have their schedule updated
Also added human readable cron property to cron triggers
2023-07-03 22:48:03 +01:00
Matt Aitken 8e147dbe0c io.sendEvent jsdocs 2023-07-03 19:19:56 +01:00
Matt Aitken 86dbd5d1dc Added JSdocs for io.wait and io.logger 2023-07-03 19:02:58 +01:00
Eric Allam e85ecfacba Remove the commented out jobs 2023-07-03 17:49:00 +01:00
Eric Allam f210532870 @trigger.dev/slack: Added joinConversation task, and automatically try and join a public conversation if the bot is not in it when posting a message 2023-07-03 17:48:14 +01:00
James Ritchie 6c9ca686cb Tweaked callout icon margin 2023-07-03 16:39:21 +01:00
James Ritchie 35b556d4a3 Using the path builder for URLs on the Jobs page 2023-07-03 16:39:21 +01:00
James Ritchie c83339404a Using the path builder for the integration docs URL 2023-07-03 16:39:21 +01:00
James Ritchie 4172def1a3 Added a docs root path to the path builder 2023-07-03 16:39:21 +01:00
James Ritchie c99481ea8d Added a callout to link to the docs 2023-07-03 16:39:21 +01:00
Matt Aitken a11ddf6564 Added JSDocs related to logging 2023-07-03 16:21:14 +01:00
Matt Aitken 917a70fbe9 Docs for the Job constructor() 2023-07-03 16:12:21 +01:00
Eric Allam 486d6818ba SDK: IO Logging now respects the job and client logLevel, and only outputs locally when ioLogLocalEnabled is true 2023-07-03 16:00:18 +01:00
Matt Aitken 807b9d4c45 SDK changeset: Added jsdocs for TriggerClient() and sendEvent() 2023-07-03 15:37:36 +01:00
Matt Aitken 0013e78b1a CLI changeset: Export the client from the generated code and added code comments 2023-07-03 15:37:25 +01:00
Matt Aitken 9dd46c83fd Added code comments to the CLI generated code, and exported the client 2023-07-03 15:35:40 +01:00
Matt Aitken 1eb6bb8aa9 Added JSDocs for TriggerCient() and client.sendEvent() 2023-07-03 15:35:40 +01:00
Eric Allam 6c86946614 SDK: Fixes issue with PING response
CLI: improves how the dev command handles register errors, also doesn’t create a tunnel when hitting local trigger.dev, and improves the dev command output
2023-07-03 15:33:44 +01:00
Eric Allam 612b016a15 When registering a schedule source < 60 seconds, make it 60 seconds 2023-07-03 14:00:46 +01:00
Eric Allam be8beacaec chore: update to Remix 1.18.1 2023-07-03 13:53:38 +01:00
Eric Allam 4d316c5524 Fixed the OAuth2 callback urls by constructing URL using x-forwarded-proto header 2023-07-03 13:47:58 +01:00
Eric Allam 7fee5d6a87 Remove logging of headers and replace integration list date with DateTime component 2023-07-03 13:26:53 +01:00
Eric Allam 0dd1aa5a5b Log out the server locale context 2023-07-03 13:11:32 +01:00
Eric Allam ed65ba4a73 isManagedCloud should be true when in the test cloud 2023-07-03 11:53:23 +01:00
Matt Aitken d39020370c Merge pull request #133 from triggerdotdev/changeset-release/main
chore: Update version for release (next)
2023-07-03 11:43:28 +01:00
github-actions[bot] c18ba11819 chore: Update version for release (next) 2023-07-03 10:29:42 +00:00
Matt Aitken 5fbd7226eb Changeset: @trigger.dev/cli: Added retrying and better messaging 2023-07-03 11:28:22 +01:00
Matt Aitken c9e27fd586 Improved the loading state of the CLI dev command. Fail after 10 connection attempts 2023-07-03 11:27:36 +01:00
Matt Aitken 51b80eb09b Deleted old CLI createProject file 2023-07-03 11:27:22 +01:00
Matt Aitken f409e9f329 Improved the messaging when it’s connecting with a retry 2023-07-02 18:54:25 +01:00
Matt Aitken 304e6aaad5 Only triggers a refresh when a relevant file is changed, retries with exponential backoff 2023-07-02 18:45:34 +01:00
Eric Allam 3c5d02043e Merge pull request #132 from triggerdotdev/changeset-release/main 2023-06-30 22:07:19 +01:00
github-actions[bot] 63980f63d4 chore: Update version for release (next) 2023-06-30 18:45:02 +00:00
Eric Allam facd2b2131 Update lockfile 2023-06-30 19:43:20 +01:00
Matt Aitken 25452588cc Update the sdk version in peer dependencies of @trigger.dev/nextjs 2023-06-30 18:05:53 +01:00
Matt Aitken 195d8c42fc Updated posthog-js to the latest 2023-06-30 18:02:03 +01:00
Matt Aitken fe04b9d32d Next.js changeset: adaptors no longer take an options object 2023-06-30 17:44:55 +01:00
Matt Aitken d03b3aeb5f Latets lockfile 2023-06-30 17:29:04 +01:00
Matt Aitken 2ee33e70f7 Fixed Slack help line highlighting 2023-06-30 17:25:57 +01:00
Matt Aitken e9209629f7 Show contextual help for an Integration 2023-06-30 17:24:55 +01:00
Matt Aitken fa1e6bf5e6 The new fields from the Integration catalog are now in the database 2023-06-30 17:24:40 +01:00
Matt Aitken 93c3b10bf7 Improved the help text some more
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-06-30 15:43:45 +01:00
Matt Aitken 56cb2e3b07 Updated the in-app quick start instructions 2023-06-30 15:38:40 +01:00
Eric Allam eabfb2457a Remove pushing to ECR 2023-06-30 15:35:20 +01:00
257 changed files with 7965 additions and 2007 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
run context jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Better docs for io.try
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Added retrying and better messaging
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
IO Logging now respects the job and client logLevel, and only outputs locally when ioLogLocalEnabled is true
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
registerCron and unregisterCron jsdocs
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/cli": patch
---
api.trigger.dev is now the default cloud url
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
io.runTask jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
[dev] Better handle errors when registering endpoint
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
io.sendEvent jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
deliverAt and timestamp event properties are now dates
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/openai": patch
---
listModels returns the response.data instead of response.data.data
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
cronTrigger jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Added JSDocs related to logging
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
intervalTrigger() jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Fixed responses from the PING action to match expected schema
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
jsdocs for DynamicTrigger and DynamicSchedule
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Export the client from the generated code and added code comments
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/github": patch
---
Fixed some properties that had API urls instead of HTML urls
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/nextjs": patch
---
Adaptors no longer take an options object
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/internal": patch
---
backgroundFetch jsdocs
+35 -2
View File
@@ -15,9 +15,14 @@
"@trigger.dev/init": "0.2.0",
"@trigger.dev/integration-kit": "2.0.0-next.1",
"@trigger.dev/resend": "1.0.0-next.1",
"@trigger.dev/cli": "0.2.1-next.8"
"@trigger.dev/cli": "0.2.1-next.8",
"@trigger.dev/typeform": "1.0.0-next.2"
},
"changesets": [
"afraid-hornets-cheer",
"beige-maps-hug",
"big-tables-search",
"bright-buses-join",
"bright-horses-bathe",
"chatty-cars-agree",
"chilly-meals-warn",
@@ -26,23 +31,51 @@
"dirty-needles-kneel",
"eight-buckets-agree",
"eighty-queens-prove",
"fast-ligers-wink",
"four-ligers-perform",
"friendly-parents-clean",
"gentle-guests-leave",
"great-meals-listen",
"healthy-rockets-cry",
"heavy-eyes-own",
"heavy-mails-explain",
"hot-eggs-protect",
"hot-seals-press",
"itchy-cars-count",
"itchy-rockets-provide",
"khaki-needles-wait",
"long-clocks-hide",
"lovely-hounds-accept",
"lovely-plants-brake",
"lucky-crews-matter",
"metal-donkeys-relate",
"modern-emus-suffer",
"modern-turtles-eat",
"moody-islands-melt",
"odd-colts-smash",
"pink-islands-pay",
"popular-cooks-boil",
"quick-stingrays-own",
"quiet-berries-tickle",
"quiet-dryers-collect",
"rich-clocks-attack",
"shiny-mirrors-beam",
"shy-bobcats-hear",
"silent-mirrors-lay",
"silver-insects-sneeze",
"six-humans-happen",
"sixty-squids-wink",
"strange-jobs-shout",
"strong-cars-visit",
"stupid-files-cough",
"sweet-cups-rescue",
"sweet-snails-refuse",
"tame-flowers-invite",
"tasty-bats-flow",
"ten-birds-confess",
"tidy-planes-compete",
"tough-walls-share",
"wild-rats-mix"
"wild-rats-mix",
"young-birds-unite"
]
}
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/cli": patch
---
Initializing a next.js project now creates a better file structure
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
registerTrigger jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
io.registerInterval and io.unregisterInterval jsdocs
+7
View File
@@ -0,0 +1,7 @@
---
"@trigger.dev/integration-kit": patch
"@trigger.dev/sdk": patch
"@trigger.dev/typeform": patch
---
Creating the typeform integration package
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Added JSdocs for io.wait and io.logger
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
isTriggerError jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
eventTrigger() jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
io.try jsdocs
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Added human readable cron expression property to cron triggers
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Added jsdocs for TriggerClient() and sendEvent()
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/slack": patch
---
Added joinConversation task, and automatically try and join a public conversation if the bot is not in it when posting a message
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Added JSdocs for Job
-38
View File
@@ -106,41 +106,3 @@ jobs:
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 }}
@@ -0,0 +1,59 @@
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
import { LinkButton } from "./primitives/Buttons";
import { Header1, Header3 } from "./primitives/Headers";
import { friendlyErrorDisplay } from "~/utils/httpErrors";
type ErrorDisplayOptions = {
button?: {
title: string;
to: string;
};
};
export function RouteErrorDisplay(options?: ErrorDisplayOptions) {
const error = useRouteError();
return (
<>
{isRouteErrorResponse(error) ? (
<ErrorDisplay
title={friendlyErrorDisplay(error.status, error.statusText).title}
message={
error.data.message ??
friendlyErrorDisplay(error.status, error.statusText).message
}
{...options}
/>
) : error instanceof Error ? (
<ErrorDisplay title={error.name} message={error.message} {...options} />
) : (
<ErrorDisplay
title="Oops"
message={JSON.stringify(error)}
{...options}
/>
)}
</>
);
}
type DisplayOptionsProps = {
title: string;
message?: string;
} & ErrorDisplayOptions;
export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
return (
<div className="p-4">
<Header1 className="mb-4 border-b border-slate-800 pb-4">{title}</Header1>
{message && <Header3>{message}</Header3>}
<LinkButton
to={button ? button.to : "/"}
variant="primary/medium"
className="mt-8"
>
{button ? button.title : "Home"}
</LinkButton>
</div>
);
}
@@ -0,0 +1,14 @@
import { H, HighlightOptions } from "highlight.run";
import { useEffect } from "react";
interface Props extends HighlightOptions {
projectId?: string;
}
export function HighlightInit({ projectId, ...highlightOptions }: Props) {
useEffect(() => {
projectId && H.init(projectId, highlightOptions);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return null;
}
@@ -23,9 +23,9 @@ export function UserAvatar({
className?: string;
}) {
return avatarUrl ? (
<div>
<div className={cn("grid aspect-square place-items-center", className)}>
<img
className={cn("aspect-square rounded-full p-[7%]", className)}
className={cn("aspect-square rounded-full p-[7%]")}
src={avatarUrl}
alt={name ?? "User"}
/>
@@ -1,10 +1,19 @@
import { Link } from "@remix-run/react";
import { Paragraph, TextLink } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { jobTestPath } from "~/utils/pathBuilder";
import { InlineCode } from "../code/InlineCode";
import { HelpPanelProps } from "../integrations/ApiKeyHelp";
import { HelpInstall } from "../integrations/HelpInstall";
import { HelpSamples } from "../integrations/HelpSamples";
import { Button, LinkButton } from "../primitives/Buttons";
import { Callout, variantClasses } from "../primitives/Callout";
import {
ClientTabs,
ClientTabsContent,
@@ -13,8 +22,6 @@ import {
} from "../primitives/ClientTabs";
import { ClipboardField } from "../primitives/ClipboardField";
import integrationButton from "./integration-button.png";
import ngrok from "./ngrok.png";
import publicUrl from "./public-url.png";
import selectEnvironment from "./select-environment.png";
import selectExample from "./select-example.png";
@@ -23,78 +30,11 @@ export function HowToSetupYourProject() {
const appOrigin = useAppOrigin();
return (
<>
<StepNumber stepNumber="1" title="Run your Next.js app" />
<StepContentContainer>
<Paragraph>
Ensure your Next.js app is running locally on port 3000.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Create a tunnel to your Next.js app" />
<StepNumber stepNumber="1" title="Run the CLI init command" />
<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.
Run this CLI command in a terminal window from your Next.js project.
</Paragraph>
<StepNumber stepNumber="a" title="Install ngrok" className="mb-2" />
<StepContentContainer>
<ClientTabs defaultValue="mac">
<ClientTabsList>
<ClientTabsTrigger value={"mac"}>Mac</ClientTabsTrigger>
<ClientTabsTrigger value={"windows"}>Windows</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"mac"}>
<ClipboardField
variant="primary/medium"
fullWidth={false}
value={`brew install ngrok/ngrok/ngrok`}
/>
</ClientTabsContent>
<ClientTabsContent value={"windows"}>
<ClipboardField
variant="primary/medium"
fullWidth={false}
value={`choco install ngrok`}
/>
</ClientTabsContent>
</ClientTabs>
</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>
<Paragraph spacing>
This creates a tunnel to your Next.js app.
</Paragraph>
<ClipboardField
variant="primary/medium"
fullWidth={false}
className="mb-4"
value={`ngrok http 3000`}
/>
</StepContentContainer>
<StepNumber
stepNumber="d"
title={`Grab your "forwarding" URL from the ngrok output`}
/>
<StepContentContainer>
<img src={ngrok} className="mt-2 w-full" />
</StepContentContainer>
</StepContentContainer>
<StepNumber stepNumber="3" title="Use the CLI" />
<StepContentContainer>
<Paragraph spacing>
Copy this CLI command into a new terminal window.{" "}
</Paragraph>
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
@@ -105,42 +45,87 @@ export function HowToSetupYourProject() {
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`npx @trigger.dev/init@latest -k ••••••••• -t ${appOrigin} -u <ngrok_forwarding_url>`}
value={`npx @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t ${appOrigin} -u <ngrok_forwarding_url>`}
secure={`npx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`npx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`pnpm dlx @trigger.dev/init@latest -k ••••••••• -t ${appOrigin} -u <ngrok_forwarding_url>`}
value={`pnpm dlx @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t ${appOrigin} -u <ngrok_forwarding_url>`}
secure={`pnpm dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`pnpm dlx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`yarn @trigger.dev/init@latest -k ••••••••• -t ${appOrigin} -u <ngrok_forwarding_url>`}
value={`yarn @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t ${appOrigin} -u <ngrok_forwarding_url>`}
secure={`yarn @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`yarn @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
</ClientTabs>
<Paragraph spacing>
Use the "forwarding" URL from step 2d above to replace the -u
placeholder text.
<Paragraph spacing variant="small">
It will ask you for a "unique ID for your endpoint". You can use the
default by hitting enter.
</Paragraph>
<img src={publicUrl} className="mb-4 mt-2 w-full" />
</StepContentContainer>
<StepNumber stepNumber="4" title="Run the CLI" />
<StepNumber stepNumber="2" title="Run your Next.js app" />
<StepContentContainer>
<Paragraph>
Ensure your Next.js app is running locally on port 3000.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI dev command" />
<StepContentContainer>
<Paragraph spacing>
It will add Trigger.dev to your existing Next.js project, setup a
route and give you an example file.
The CLI <InlineCode>dev</InlineCode> command allows the Trigger.dev
service to send messages to your Next.js site. This is required for
registering Jobs, triggering them and running Tasks. To achieve this
it creates a tunnel (using{" "}
<TextLink href="https://ngrok.com/">ngrok</TextLink>) so Trigger.dev
can send messages to your machine.
</Paragraph>
<Paragraph spacing>
You should leave the <InlineCode>dev</InlineCode> command running when
you're developing.
</Paragraph>
<Paragraph spacing>
In a{" "}
<strong className="text-bright">new terminal window or tab</strong>{" "}
run:
</Paragraph>
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`npx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
</ClientTabs>
</StepContentContainer>
<StepNumber stepNumber="5" title="Check for Jobs" />
<StepNumber stepNumber="4" title="Check for Jobs" />
<StepContentContainer>
<Paragraph>
Once you've run the CLI command, click Refresh to view your example
@@ -159,6 +144,65 @@ export function HowToSetupYourProject() {
);
}
export function HowToRunYourJob() {
const organization = useOrganization();
const project = useProject();
const job = useJob();
return (
<>
<Callout variant="info" className="mb-6">
<Paragraph variant={"small"} className={variantClasses.info.textColor}>
Scheduled Triggers <strong>do not</strong> trigger Jobs in the DEV
Environment. When developing locally you should use the{" "}
<Link
to={jobTestPath(organization, project, job)}
className="underline underline-offset-2 transition hover:text-blue-100"
>
Test feature
</Link>{" "}
to trigger any scheduled Jobs.
</Paragraph>
</Callout>
<Paragraph spacing>There are two ways to run your Job:</Paragraph>
<StepNumber stepNumber="1" title="Trigger a test Run" />
<StepContentContainer>
<Paragraph spacing>
You can perform a Run with any payload you want, or use one of our
examples, on the test page.
</Paragraph>
<LinkButton
to={jobTestPath(organization, project, job)}
variant={"primary/small"}
LeadingIcon={"beaker"}
leadingIconClassName="text-bright"
>
Test
</LinkButton>
</StepContentContainer>
<StepNumber stepNumber="2" title="Trigger your Job for real" />
<StepContentContainer>
<Paragraph spacing>
Performing a real run depends on the type of Trigger your Job is
using.
</Paragraph>
<LinkButton
to="https://trigger.dev/docs/documentation/guides/running-jobs"
variant={"primary/small"}
LeadingIcon={"docs"}
leadingIconClassName="text-bright"
>
How to run a Job
</LinkButton>
</StepContentContainer>
</>
);
}
export function HowToRunATest() {
return (
<>
@@ -190,7 +234,7 @@ export function HowToRunATest() {
</StepContentContainer>
<Callout
variant="docs"
href="https://trigger.dev/docs/documentation/guides/testing-jobs"
to="https://trigger.dev/docs/documentation/guides/testing-jobs"
>
Learn more about running tests.
</Callout>
@@ -238,7 +282,7 @@ export function HowToConnectAnIntegration() {
</StepContentContainer>
<Callout
variant={"docs"}
href="https://trigger.dev/docs/integrations/introduction"
to="https://trigger.dev/docs/integrations/introduction"
>
View the Integration docs page for more information on connecting an API
using an Integration or other method.
@@ -247,13 +291,29 @@ export function HowToConnectAnIntegration() {
);
}
export function HowToUseThisIntegration() {
export function HowToUseThisIntegration({
integration,
help,
integrationClient,
}: HelpPanelProps) {
return (
<>
<StepNumber stepNumber="1" title="Step 1 title" />
<StepNumber stepNumber="1" title="Install the package" />
<StepContentContainer>
<Paragraph>Content</Paragraph>
<HelpInstall packageName={integration.packageName} />
</StepContentContainer>
{help && (
<>
<StepNumber stepNumber="2" title="Create a Job" />
<StepContentContainer>
<HelpSamples
help={help}
integration={integration}
integrationClient={integrationClient}
/>
</StepContentContainer>
</>
)}
</>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

@@ -1,26 +1,26 @@
import {
ApiAuthenticationMethodApiKey,
Integration,
} from "~/services/externalApis/types";
import { CodeBlock } from "../code/CodeBlock";
import { Help, Integration } from "~/services/externalApis/types";
import { InlineCode } from "../code/InlineCode";
import { ClipboardField } from "../primitives/ClipboardField";
import { Header1 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
import {
ClientTabs,
ClientTabsContent,
ClientTabsList,
ClientTabsTrigger,
} from "../primitives/ClientTabs";
import { HelpInstall } from "./HelpInstall";
import { HelpSamples, ReplacementData } from "./HelpSamples";
export type HelpPanelIntegration = Pick<
Integration,
"name" | "packageName" | "identifier"
>;
export type HelpPanelProps = {
integration: HelpPanelIntegration;
help?: Help;
integrationClient?: ReplacementData;
};
export function ApiKeyHelp({
integration,
apiAuth,
}: {
integration: Integration;
apiAuth: ApiAuthenticationMethodApiKey;
}) {
help,
integrationClient,
}: HelpPanelProps) {
return (
<div className="mt-4">
<Header1 className="mb-2">
@@ -34,44 +34,14 @@ export function ApiKeyHelp({
First install the <InlineCode>{integration.packageName}</InlineCode>{" "}
package using your preferred package manager. For example:
</Paragraph>
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="secondary/medium"
value={`npm install ${integration.packageName}@next`}
className="mb-4"
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="secondary/medium"
value={`pnpm install ${integration.packageName}@next`}
className="mb-4"
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="secondary/medium"
value={`yarn add ${integration.packageName}@next`}
className="mb-4"
/>
</ClientTabsContent>
</ClientTabs>
{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>
))}
<HelpInstall packageName={integration.packageName} />
{help && (
<HelpSamples
help={help}
integration={integration}
integrationClient={integrationClient}
/>
)}
</div>
);
}
@@ -129,12 +129,7 @@ function SelectedIntegrationMethod({
case "apikey":
const apiAuth = authMethods.find((a) => a.type === "apikey");
if (!apiAuth) return null;
return (
<ApiKeyHelp
integration={integration}
apiAuth={apiAuth as ApiAuthenticationMethodApiKey}
/>
);
return <ApiKeyHelp integration={integration} help={apiAuth.help} />;
case "oauth2":
return (
<SelectOAuthMethod
@@ -0,0 +1,45 @@
import { Integration } from "~/services/externalApis/types";
import { InlineCode } from "../code/InlineCode";
import {
ClientTabs,
ClientTabsList,
ClientTabsTrigger,
ClientTabsContent,
} from "../primitives/ClientTabs";
import { ClipboardField } from "../primitives/ClipboardField";
import { Paragraph } from "../primitives/Paragraph";
export function HelpInstall({ packageName }: { packageName: string }) {
return (
<>
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="secondary/medium"
value={`npm install ${packageName}`}
className="mb-4"
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="secondary/medium"
value={`pnpm install ${packageName}`}
className="mb-4"
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="secondary/medium"
value={`yarn add ${packageName}`}
className="mb-4"
/>
</ClientTabsContent>
</ClientTabs>
</>
);
}
@@ -0,0 +1,62 @@
import { Integration } from "~/services/externalApis/types";
import { HelpPanelIntegration, HelpPanelProps } from "./ApiKeyHelp";
import { Paragraph } from "../primitives/Paragraph";
import { CodeBlock } from "../code/CodeBlock";
export type ReplacementData = {
slug: string;
};
export function HelpSamples({
help,
integrationClient,
integration,
}: HelpPanelProps) {
return (
<>
{help &&
help.samples.map((sample, i) => {
const code = runReplacers(
sample.code,
integrationClient,
integration
);
return (
<div key={i}>
<Paragraph spacing>{sample.title}</Paragraph>
<CodeBlock
code={code}
className="mb-4"
highlightedRanges={sample.highlight}
/>
</div>
);
})}
</>
);
}
const replacements = [
{
match: /__SLUG__/g,
replacement: (
data: ReplacementData | undefined,
integration: HelpPanelIntegration
) => {
if (data) return data.slug;
return integration.identifier;
},
},
];
function runReplacers(
code: string,
replacementData: ReplacementData | undefined,
integration: HelpPanelIntegration
) {
replacements.forEach((r) => {
code = code.replace(r.match, r.replacement(replacementData, integration));
});
return code;
}
+33 -10
View File
@@ -18,6 +18,7 @@ import { runStatusTitle } from "../runs/RunStatuses";
import { ProjectJob, useProject } from "~/hooks/useProject";
import { useOrganization } from "~/hooks/useOrganizations";
import { JobRunStatus } from "~/models/job.server";
import { cn } from "~/utils/cn";
export function JobsTable({
jobs,
@@ -46,7 +47,12 @@ export function JobsTable({
jobs.map((job) => {
const path = jobPath(organization, project, job);
return (
<TableRow key={job.id}>
<TableRow
key={job.id}
className={cn(
job.hasIntegrationsRequiringAction && "bg-rose-500/30"
)}
>
<TableCell to={path}>
<span className="flex items-center gap-2">
<NamedIcon name={job.event.icon} className="h-8 w-8" />
@@ -79,12 +85,30 @@ export function JobsTable({
<SimpleTooltip
key={integration.key}
button={
<NamedIcon
name={integration.icon}
className="h-6 w-6"
/>
<div className="relative">
<NamedIcon
name={integration.icon}
className="h-6 w-6"
/>
{integration.setupStatus === "MISSING_FIELDS" && (
<NamedIcon
name="error"
className="absolute -left-1 -top-1 h-4 w-4"
/>
)}
</div>
}
content={
<div>
<p className="mb-1 text-rose-400">
{integration.setupStatus === "MISSING_FIELDS" &&
"This integration requires configuration"}
</p>
<p>
{integration.title}: {integration.key}
</p>
</div>
}
content={`${integration.title}: ${integration.key}`}
/>
))}
</TableCell>
@@ -126,10 +150,9 @@ export function JobsTable({
</span>
}
value={
<DateTime
date={job.lastRun.createdAt}
className={classForJobStatus(job.lastRun.status)}
/>
<span className={classForJobStatus(job.lastRun.status)}>
<DateTime date={job.lastRun.createdAt} />
</span>
}
/>
) : (
@@ -102,73 +102,77 @@ function BreadcrumbItem({
case "projects":
return <ProjectsMenu key={breadcrumb.slug} />;
case "jobs":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectPath(organization!, project!)}
to={projectPath(organization, project)}
title="Jobs"
/>
);
case "environments":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectEnvironmentsPath(organization!, project!)}
to={projectEnvironmentsPath(organization, project)}
title="Environments"
/>
);
case "integrations":
if (!organization || !project) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={projectIntegrationsPath(organization!, project!)}
to={projectIntegrationsPath(organization, project)}
title="Integrations"
/>
);
case "integration":
if (!organization || !project || !client) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={projectIntegrationsPath(organization!, project!)}
to={projectIntegrationsPath(organization, project)}
title="Integrations"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={integrationClientPath(organization!, project!, client!)}
title={client!.title}
to={integrationClientPath(organization, project, client)}
title={client.title}
/>
</Fragment>
);
case "integration-job":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientPath(organization!, project!, client!)}
to={integrationClientPath(organization, project, client)}
title={"Jobs"}
/>
);
case "integration-connections":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientConnectionsPath(
organization!,
project!,
client!
)}
to={integrationClientConnectionsPath(organization, project, client)}
title={"Connections"}
/>
);
case "integration-scopes":
if (!organization || !project || !client) return null;
return (
<BreadcrumbLink
to={integrationClientScopesPath(organization!, project!, client!)}
to={integrationClientScopesPath(organization, project, client)}
title={"Scopes"}
/>
);
case "job":
if (!organization || !project || !job) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={projectPath(organization!, project!)}
to={projectPath(organization, project)}
title="Jobs"
/>
<BreadcrumbIcon />
@@ -176,32 +180,35 @@ function BreadcrumbItem({
</Fragment>
);
case "test":
if (!organization || !project || !job) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={jobTestPath(organization!, project!, job!)}
to={jobTestPath(organization, project, job)}
title="Test"
/>
);
case "runs":
if (!organization || !project || !job) return null;
return (
<BreadcrumbLink
key={breadcrumb.slug}
to={jobPath(organization!, project!, job!)}
to={jobPath(organization, project, job)}
title="Runs"
/>
);
case "run":
if (!organization || !project || !job || !run) return null;
return (
<Fragment key={breadcrumb.slug}>
<BreadcrumbLink
to={jobPath(organization!, project!, job!)}
to={jobPath(organization, project, job)}
title="Runs"
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={runDashboardPath(organization!, project!, job!, run!)}
title={`Run #${run!.number}`}
to={runDashboardPath(organization, project, job, run)}
title={`Run #${run.number}`}
/>
</Fragment>
);
@@ -1,7 +1,7 @@
import { useMatches } from "@remix-run/react";
import { motion } from "framer-motion";
import invariant from "tiny-invariant";
import { useJob, useOptionalJob } from "~/hooks/useJob";
import { useOptionalIntegrationClient } from "~/hooks/useIntegrationClient";
import { useOptionalJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
@@ -13,11 +13,10 @@ import {
projectIntegrationsPath,
projectPath,
} from "~/utils/pathBuilder";
import { UserProfilePhoto } from "../UserProfilePhoto";
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 (
@@ -72,18 +71,21 @@ export function ProjectSideMenu() {
to={projectPath(organization, project)}
isCollapsed={isCollapsed}
forceActive={jobsActive}
data-action="jobs"
/>
<SideMenuItem
name="Integrations"
icon="integration"
to={projectIntegrationsPath(organization, project)}
isCollapsed={isCollapsed}
data-action="integrations"
/>
<SideMenuItem
name="Environments & API Keys"
icon="environment"
to={projectEnvironmentsPath(organization, project)}
isCollapsed={isCollapsed}
data-action="environments & api keys"
/>
</div>
<div className="flex flex-col">
@@ -92,18 +94,21 @@ export function ProjectSideMenu() {
icon="team"
to={organizationTeamPath(organization)}
isCollapsed={isCollapsed}
data-action="team"
/>
<SideMenuItem
name="Usage & Billing"
icon="billing"
to={organizationBillingPath(organization)}
isCollapsed={isCollapsed}
data-action="usage & billing"
/>
<SideMenuItem
name="Account"
icon={UserProfilePhoto}
to={accountPath()}
isCollapsed={isCollapsed}
data-action="account"
/>
</div>
</motion.div>
@@ -4,49 +4,56 @@ import {
InformationCircleIcon,
LightBulbIcon,
} from "@heroicons/react/20/solid";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import {
ArrowTopRightOnSquareIcon,
BookOpenIcon,
CheckCircleIcon,
ChevronRightIcon,
} from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
const variantClasses = {
export const variantClasses = {
info: {
className: "border-blue-400/20 bg-blue-400/30",
icon: <InformationCircleIcon className="h-5 w-5 shrink-0 text-blue-400" />,
text: "text-blue-200",
textColor: "text-blue-200",
linkClassName: "transition hover:bg-blue-400/40",
},
warning: {
className: "border-yellow-400/20 bg-yellow-400/30",
icon: (
<ExclamationCircleIcon className="h-5 w-5 shrink-0 text-yellow-400" />
),
text: "text-yellow-200",
textColor: "text-yellow-200",
linkClassName: "transition hover:bg-yellow-400/40",
},
error: {
className: "border-[hsl(347,77%,50%)]/30 bg-[hsl(347,77%,50%)]/20",
className: "border-rose-500/20 bg-rose-500/30",
icon: (
<ExclamationTriangleIcon className="h-5 w-5 shrink-0 text-rose-400" />
),
text: "text-rose-200",
textColor: "text-rose-200",
linkClassName: "transition hover:bg-rose-500/40",
},
idea: {
className: "border-green-400/20 bg-green-400/30",
icon: <LightBulbIcon className="h-5 w-5 shrink-0 text-green-400" />,
text: "text-green-200",
textColor: "text-green-200",
linkClassName: "transition hover:bg-green-400/40",
},
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",
textColor: "text-green-200",
linkClassName: "transition hover:bg-green-400/40",
},
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",
className: "border-blue-400/20 bg-blue-400/30",
icon: <BookOpenIcon className="mt-0.5 h-5 w-5 shrink-0 text-blue-400" />,
textColor: "text-blue-200",
linkClassName: "transition hover:bg-blue-400/40",
},
} as const;
@@ -55,41 +62,79 @@ export function Callout({
className,
icon,
variant,
href,
to,
}: {
children?: React.ReactNode;
className?: string;
icon?: React.ReactNode;
variant: keyof typeof variantClasses;
href?: string;
to?: 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-start gap-x-2">
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.text}>
{children}
</Paragraph>
) : (
children
if (to !== undefined) {
if (to.startsWith("http")) {
return (
<a
href={to}
target="_blank"
className={cn(
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
variantDefinition.className,
variantDefinition.linkClassName,
className
)}
</div>
<ArrowTopRightOnSquareIcon className="h-5 w-5 text-blue-400" />
</a>
);
>
<div className={"flex w-full items-start gap-x-2"}>
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph
variant={"small"}
className={variantDefinition.textColor}
>
{children}
</Paragraph>
) : (
children
)}
</div>
<ArrowTopRightOnSquareIcon
className={cn("h-5 w-5", variantDefinition.textColor)}
/>
</a>
);
} else {
return (
<Link
to={to}
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,
variantDefinition.linkClassName,
className
)}
>
<div className={"flex w-full items-start gap-x-2"}>
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph
variant={"small"}
className={variantDefinition.textColor}
>
{children}
</Paragraph>
) : (
children
)}
</div>
<ChevronRightIcon
className={cn("h-5 w-5", variantDefinition.textColor)}
/>
</Link>
);
}
}
return (
@@ -103,7 +148,7 @@ export function Callout({
{icon ? icon : variantDefinition.icon}
{typeof children === "string" ? (
<Paragraph variant={"small"} className={variantDefinition.text}>
<Paragraph variant={"small"} className={variantDefinition.textColor}>
{children}
</Paragraph>
) : (
@@ -1,60 +1,114 @@
import { Fragment, useEffect, useState } from "react";
import { useLocales } from "./LocaleProvider";
type DateTimeProps = {
date: Date | string;
timeZone?: string;
className?: string;
includeSeconds?: boolean;
};
export const DateTime = ({ date, timeZone, className }: DateTimeProps) => {
export const DateTime = ({
date,
timeZone = "UTC",
includeSeconds = true,
}: DateTimeProps) => {
const locales = useLocales();
const realDate = typeof date === "string" ? new Date(date) : date;
const locales = useLocales();
const isoString = realDate.toISOString();
const formattedDate = new Intl.DateTimeFormat(locales, {
const initialFormattedDateTime = formatDateTime(
realDate,
timeZone,
locales,
includeSeconds
);
const [formattedDateTime, setFormattedDateTime] = useState<string>(
initialFormattedDateTime
);
useEffect(() => {
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
setFormattedDateTime(
formatDateTime(
realDate,
resolvedOptions.timeZone,
locales,
includeSeconds
)
);
}, [locales, includeSeconds, realDate]);
return (
<Fragment>
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
</Fragment>
);
};
function formatDateTime(
date: Date,
timeZone: string,
locales: string[],
includeSeconds: boolean
): string {
return new Intl.DateTimeFormat(locales, {
year: "numeric",
month: "short",
day: "numeric",
timeZone,
}).format(realDate);
const formattedTime = new Intl.DateTimeFormat(locales, {
hour: "numeric",
minute: "numeric",
second: includeSeconds ? "numeric" : undefined,
timeZone,
}).format(realDate);
}).format(date);
}
export const DateTimeAccurate = ({ date, timeZone = "UTC" }: DateTimeProps) => {
const locales = useLocales();
const realDate = typeof date === "string" ? new Date(date) : date;
const initialFormattedDateTime = formatDateTimeAccurate(
realDate,
timeZone,
locales
);
const [formattedDateTime, setFormattedDateTime] = useState<string>(
initialFormattedDateTime
);
useEffect(() => {
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
setFormattedDateTime(
formatDateTimeAccurate(realDate, resolvedOptions.timeZone, locales)
);
}, [locales, realDate]);
return (
<time dateTime={isoString} className={className}>
{formattedDate} at {formattedTime}
</time>
<Fragment>
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
</Fragment>
);
};
export const DateTimeAccurate = ({ date, timeZone }: DateTimeProps) => {
const realDate = typeof date === "string" ? new Date(date) : date;
function formatDateTimeAccurate(
date: Date,
timeZone: string,
locales: string[]
): string {
const milliseconds = `00${date.getMilliseconds()}`.slice(-3);
const locales = useLocales();
const isoString = realDate.toISOString();
const formattedDate = new Intl.DateTimeFormat(locales, {
const formattedDateTime = new Intl.DateTimeFormat(locales, {
month: "short",
day: "2-digit",
timeZone,
}).format(realDate);
const formattedTime = new Intl.DateTimeFormat(locales, {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZone,
}).format(realDate);
}).format(date);
const milliseconds = `00${realDate.getMilliseconds()}`.slice(-3);
return (
<time dateTime={isoString}>
{formattedDate} {formattedTime}.{milliseconds}
</time>
);
};
return `${formatDateTime}.${milliseconds}`;
}
@@ -52,7 +52,7 @@ export function HelpTrigger({ title }: { title: string }) {
<Button
variant="tertiary/small"
LeadingIcon="lightbulb"
leadingIconClassName="text-slate-400"
leadingIconClassName="text-amber-400"
onClick={() => setOpen(true)}
>
{title}
@@ -1,6 +1,6 @@
import * as React from "react";
import { cn } from "~/utils/cn";
import type { IconNames } from "./NamedIcon";
import type { IconNamesOrString } from "./NamedIcon";
import { NamedIcon } from "./NamedIcon";
const variants = {
@@ -44,7 +44,7 @@ const variants = {
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
variant?: keyof typeof variants;
icon?: IconNames;
icon?: IconNamesOrString;
shortcut?: string;
fullWidth?: boolean;
};
@@ -1,4 +1,5 @@
import {
ArrowTopRightOnSquareIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
@@ -9,7 +10,6 @@ import {
ArrowLeftIcon,
ArrowPathIcon,
ArrowRightIcon,
ArrowTopRightOnSquareIcon,
BeakerIcon,
BellAlertIcon,
BoltIcon,
@@ -39,6 +39,7 @@ import {
MagnifyingGlassIcon,
PlusIcon,
PlusSmallIcon,
QrCodeIcon,
SquaresPlusIcon,
StarIcon,
UserCircleIcon,
@@ -103,7 +104,7 @@ const icons = {
<XMarkIcon className={cn("text-dimmed", className)} />
),
"connection-alert": (className: string) => (
<BellAlertIcon className={cn("text-rose-500", className)} />
<BellAlertIcon className={cn("text-amber-500", className)} />
),
docs: (className: string) => (
<BookOpenIcon className={cn("text-slate-400", className)} />
@@ -175,6 +176,9 @@ const icons = {
property: (className: string) => (
<Cog8ToothIcon className={cn("text-slate-600", className)} />
),
"qr-code": (className: string) => (
<QrCodeIcon className={cn("text-amber-400", className)} />
),
refresh: (className: string) => (
<ArrowPathIcon className={cn("text-bright", className)} />
),
@@ -224,6 +228,7 @@ const icons = {
};
export type IconNames = keyof typeof icons;
export type IconNamesOrString = IconNames | (string & {});
export const iconNames = Object.keys(icons) as IconNames[];
export function NamedIcon({
@@ -231,7 +236,7 @@ export function NamedIcon({
className,
fallback,
}: {
name: IconNames | (string & {});
name: IconNamesOrString;
className: string;
fallback?: JSX.Element;
}) {
@@ -241,7 +246,7 @@ export function NamedIcon({
if (hasIcon(name)) {
return (
<div
<span
className={cn(
"grid aspect-square min-h-fit place-items-center",
className
@@ -255,7 +260,7 @@ export function NamedIcon({
shapeRendering: "geometricPrecision",
}}
/>
</div>
</span>
);
}
@@ -120,11 +120,7 @@ export function PageInfoProperty({
{label}:
</Paragraph>
)}
{typeof value === "string" || typeof value === "number" ? (
<Paragraph variant="small">{value}</Paragraph>
) : (
value
)}
<Paragraph variant="small">{value}</Paragraph>
</div>
);
}
@@ -1,4 +1,6 @@
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
const paragraphVariants = {
base: {
@@ -90,18 +92,47 @@ export function Paragraph({
}
type TextLinkProps = {
href: string;
href?: string;
to?: string;
className?: string;
trailingIcon?: IconNamesOrString;
trailingIconClassName?: string;
children: React.ReactNode;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
export function TextLink({ href, children, ...props }: TextLinkProps) {
return (
<a
href={href}
className="text-indigo-500 transition hover:text-indigo-400"
{...props}
>
{children}
const classes =
"text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center group";
export function TextLink({
href,
to,
children,
className,
trailingIcon,
trailingIconClassName,
...props
}: TextLinkProps) {
return to ? (
<Link to={to} className={cn(classes, className)} {...props}>
{children}{" "}
{trailingIcon && (
<NamedIcon
name={trailingIcon}
className={cn("h-4 w-4", trailingIconClassName)}
/>
)}
</Link>
) : href ? (
<a href={href} className={cn(classes, className)} {...props}>
{children}{" "}
{trailingIcon && (
<NamedIcon
name={trailingIcon}
className={cn("h-4 w-4", trailingIconClassName)}
/>
)}
</a>
) : (
<span>Need to define a path or href</span>
);
}
@@ -4,10 +4,11 @@ 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 { formatDuration } from "~/utils";
import { runDashboardPath } from "~/utils/pathBuilder";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
import { Callout } from "../primitives/Callout";
import { DateTime } from "../primitives/DateTime";
import { Spinner } from "../primitives/Spinner";
import {
Table,
@@ -56,11 +57,11 @@ export function RunsTable({
<TableBody>
{total === 0 && !hasFilters ? (
<TableBlankRow colSpan={8}>
<NoRuns title="No runs found for this Job" />
<NoRuns title="No Runs found for this Job" />
</TableBlankRow>
) : runs.length === 0 ? (
<TableBlankRow colSpan={8}>
<NoRuns title="No runs match your filters" />
<NoRuns title="No Runs match your filters" />
</TableBlankRow>
) : (
runs.map((run) => {
@@ -75,9 +76,7 @@ export function RunsTable({
<RunStatus status={run.status} />
</TableCell>
<TableCell to={path}>
{run.startedAt
? formatDateTime(run.startedAt, "medium")
: ""}
{run.startedAt ? <DateTime date={run.startedAt} /> : ""}
</TableCell>
<TableCell to={path}>
{formatDuration(run.startedAt, run.completedAt, {
@@ -93,9 +92,7 @@ export function RunsTable({
</TableCell>
<TableCell to={path}>{run.version}</TableCell>
<TableCell to={path}>
{run.createdAt
? formatDateTime(run.createdAt, "medium")
: ""}
{run.createdAt ? <DateTime date={run.createdAt} /> : ""}
</TableCell>
<TableCellChevron to={path} />
</TableRow>
@@ -20,17 +20,44 @@ export const Callouts: Story = {
function CalloutsSet() {
return (
<div className="flex flex-col items-start gap-y-4 p-4">
<Callout variant="info">This is an info callout</Callout>
<Callout variant="warning">This is a warning callout</Callout>
<Callout variant="error">This is an error callout</Callout>
<Callout variant="idea">This is an idea callout</Callout>
<Callout
variant="idea"
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
>
This has a custom icon
</Callout>
<div className="grid grid-cols-2">
<div className="flex flex-col items-start gap-y-4 p-4">
<Callout variant="info">This is an info callout</Callout>
<Callout variant="warning">This is a warning callout</Callout>
<Callout variant="error">This is an error callout</Callout>
<Callout variant="idea">This is an idea callout</Callout>
<Callout variant="docs">This is a docs callout</Callout>
<Callout
variant="idea"
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
>
This has a custom icon
</Callout>
</div>
<div className="flex flex-col items-start gap-y-4 p-4">
<Callout to="#" variant="info">
This is an info callout
</Callout>
<Callout to="#" variant="warning">
This is a warning callout
</Callout>
<Callout to="#" variant="error">
This is an error callout
</Callout>
<Callout to="#" variant="idea">
This is an idea callout
</Callout>
<Callout to="#" variant="docs">
This is a docs callout
</Callout>
<Callout
to="#"
variant="idea"
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
>
This has a custom icon
</Callout>
</div>
</div>
);
}
@@ -70,10 +70,14 @@ function Typography({ header1, header2, header3, paragraph }: TypographyProps) {
TextLink. It takes an href and children.
</Paragraph>
<Paragraph>
Learn how to get started quickly using the included some example Jobs
which are great as a quick start project. You can check them out in
your project here in triggerdotdev/jobs/examples. You can also see the
examples in more detail in the docs.
Learn how to get{" "}
<TextLink href="#" trailingIcon="external-link">
started quickly
</TextLink>{" "}
using the included some example Jobs which are great as a quick start
project. You can check them out in your project here in
triggerdotdev/jobs/examples. You can also see the examples in more
detail in the docs.
</Paragraph>
</div>
<div>
+44 -4
View File
@@ -1,6 +1,7 @@
import { PrismaClient, Prisma } from "@trigger.dev/database";
import invariant from "tiny-invariant";
import { z } from "zod";
import { logger } from "./services/logger.server";
export type PrismaTransactionClient = Omit<
PrismaClient,
@@ -15,15 +16,54 @@ function isTransactionClient(
return !("$transaction" in prisma);
}
export function $transaction<R>(
function isPrismaKnownError(
error: unknown
): error is Prisma.PrismaClientKnownRequestError {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
typeof error.code === "string"
);
}
export type PrismaTransactionOptions = {
/** The maximum amount of time (in ms) Prisma Client will wait to acquire a transaction from the database. The default value is 2000ms. */
maxWait?: number;
/** The maximum amount of time (in ms) the interactive transaction can run before being canceled and rolled back. The default value is 5000ms. */
timeout?: number;
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
isolationLevel?: Prisma.TransactionIsolationLevel;
};
export async function $transaction<R>(
prisma: PrismaClientOrTransaction,
fn: (prisma: PrismaTransactionClient) => Promise<R>
): Promise<R> {
fn: (prisma: PrismaTransactionClient) => Promise<R>,
options?: PrismaTransactionOptions
): Promise<R | undefined> {
if (isTransactionClient(prisma)) {
return fn(prisma);
}
return (prisma as PrismaClient).$transaction(fn);
try {
return await (prisma as PrismaClient).$transaction(fn, options);
} catch (error) {
if (isPrismaKnownError(error)) {
logger.debug("prisma.$transaction error", {
code: error.code,
meta: error.meta,
stack: error.stack,
message: error.message,
name: error.name,
});
return;
}
throw error;
}
}
export { Prisma };
+1 -3
View File
@@ -1,7 +1,5 @@
import { RemixBrowser, useLocation, useMatches } from "@remix-run/react";
import { RemixBrowser } from "@remix-run/react";
import { hydrateRoot } from "react-dom/client";
import { useEffect } from "react";
import posthog from "posthog-js";
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
import { OperatingSystemContextProvider } from "./components/primitives/OperatingSystemProvider";
+45 -8
View File
@@ -1,16 +1,18 @@
import * as Worker from "~/services/worker.server";
import { PassThrough } from "stream";
import { renderToPipeableStream } from "react-dom/server";
import { RemixServer } from "@remix-run/react";
import { H } from "@highlight-run/node";
import type { DataFunctionArgs, EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
import { Response } from "@remix-run/node"; // or cloudflare/deno
import type { EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
import { RemixServer } from "@remix-run/react";
import { parseAcceptLanguage } from "intl-parse-accept-language";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import { PassThrough } from "stream";
import * as Worker from "~/services/worker.server";
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
import {
OperatingSystemContextProvider,
OperatingSystemPlatform,
} from "./components/primitives/OperatingSystemProvider";
import { env } from "./env.server";
const ABORT_DELAY = 30000;
@@ -107,6 +109,7 @@ function serveBrowsers(
) {
return new Promise((resolve, reject) => {
let didError = false;
let shellReady = false;
const { pipe, abort } = renderToPipeableStream(
<OperatingSystemContextProvider platform={platform}>
<LocaleContextProvider locales={locales}>
@@ -120,6 +123,7 @@ function serveBrowsers(
{
// use onShellReady to wait until a suspense boundary is triggered
onShellReady() {
shellReady = true;
responseHeaders.set("Content-Type", "text/html");
let body = new PassThrough();
pipe(body);
@@ -133,9 +137,11 @@ function serveBrowsers(
onShellError(err: unknown) {
reject(err);
},
onError(err: unknown) {
onError(error: unknown) {
didError = true;
console.error(err);
if (shellReady) {
logError(error, request);
}
},
}
);
@@ -143,4 +149,35 @@ function serveBrowsers(
});
}
Worker.init().catch(console.error);
if (env.HIGHLIGHT_PROJECT_ID) {
H.init({ projectID: env.HIGHLIGHT_PROJECT_ID });
}
export function handleError(
error: unknown,
{ request, params, context }: DataFunctionArgs
) {
logError(error, request);
}
Worker.init().catch((error) => {
logError(error);
});
function logError(error: unknown, request?: Request) {
if (env.HIGHLIGHT_PROJECT_ID) {
const parsed = request
? H.parseHeaders(Object.fromEntries(request.headers))
: undefined;
if (error instanceof Error) {
H.consumeError(error, parsed?.secureSessionId, parsed?.requestId);
} else {
H.consumeError(
new Error(`Unknown error: ${JSON.stringify(error)}`),
parsed?.secureSessionId,
parsed?.requestId
);
}
}
console.error(error);
}
+1
View File
@@ -27,6 +27,7 @@ const EnvironmentSchema = z.object({
.default(process.env.NODE_ENV),
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
POSTHOG_PROJECT_KEY: z.string().optional(),
HIGHLIGHT_PROJECT_ID: z.string().optional(),
MAGIC_LINK_SECRET: z.string(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
+4 -2
View File
@@ -1,4 +1,4 @@
import { requestUrl } from "./utils";
import { requestUrl } from "./utils/requestUrl.server";
export type TriggerFeatures = {
isManagedCloud: boolean;
@@ -10,7 +10,9 @@ export function featuresForRequest(request: Request): TriggerFeatures {
const url = requestUrl(request);
const isManagedCloud =
url.host === "cloud.trigger.dev" || process.env.NODE_ENV === "development";
url.host === "cloud.trigger.dev" ||
url.host === "test-cloud.trigger.dev" ||
process.env.CLOUD_ENV === "development";
return {
isManagedCloud,
+26
View File
@@ -0,0 +1,26 @@
import { useEffect, useRef } from "react";
/** Call a function when the id of the item changes */
export function useChanged<T extends { id: string }>(
getItem: () => T | undefined,
action: (item: T | undefined) => void,
sendInitialUndefined = true
) {
const previousItemId = useRef<string | undefined>();
const item = getItem();
//when the value changes, call the action
useEffect(() => {
if (previousItemId.current !== item?.id) {
action(item);
}
previousItemId.current = item?.id;
}, [item]);
//if sendInitialUndefined is true, call the action when the component first renders
useEffect(() => {
if (item !== undefined || sendInitialUndefined === false) return;
action(item);
}, []);
}
+14
View File
@@ -0,0 +1,14 @@
import { H } from "highlight.run";
import { useUserChanged } from "./useUser";
export function useHighlight() {
useUserChanged((user) => {
if (!user) {
return;
}
H.identify(user.id, {
email: user.email,
});
});
}
@@ -16,7 +16,7 @@ export function useOptionalIntegrationClient() {
}
export function useIntegrationClient() {
const client = useOptionalIntegrationClient();
invariant(client, "Client must be defined");
return client;
const integration = useOptionalIntegrationClient();
invariant(integration, "Integration must be defined");
return integration;
}
+7
View File
@@ -5,6 +5,7 @@ import {
import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
import { useOptionalProject } from "./useProject";
import { useChanged } from "./useChanged";
export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
@@ -31,3 +32,9 @@ export function useJob() {
invariant(job, "Job must be defined");
return job;
}
export const useJobChanged = (
action: (org: MatchedJob | undefined) => void
) => {
useChanged(useOptionalJob, action);
};
@@ -6,6 +6,7 @@ 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";
import { useChanged } from "./useChanged";
export type MatchedOrganization = UseDataFunctionReturn<
typeof appLoader
@@ -59,3 +60,9 @@ function useOrganizationsFromMatchesData(paths: string[]) {
UseDataFunctionReturn<typeof appLoader>["organizations"]
>(routeMatch.data.organizations);
}
export const useOrganizationChanged = (
action: (org: MatchedOrganization | undefined) => void
) => {
useChanged(useOptionalOrganization, action);
};
+48 -27
View File
@@ -1,62 +1,83 @@
import { useLocation } from "@remix-run/react";
import posthog from "posthog-js";
import { useEffect, useRef } from "react";
import { useOptionalOrganization } from "./useOrganizations";
import { useOptionalUser } from "./useUser";
import { useOrganizationChanged } from "./useOrganizations";
import { useOptionalUser, useUserChanged } from "./useUser";
import { useProjectChanged } from "./useProject";
import { useJobChanged } from "./useJob";
export const usePostHog = (apiKey?: string, logging = false): void => {
export const usePostHog = (
apiKey?: string,
logging = false,
debug = false
): void => {
const postHogInitialized = useRef(false);
const location = useLocation();
const user = useOptionalUser();
const currentOrganization = useOptionalOrganization();
const currentOrganizationId = currentOrganization?.id;
//start PostHog once
useEffect(() => {
if (apiKey === undefined) return;
if (apiKey === "") return;
if (apiKey === undefined || apiKey === "") return;
if (postHogInitialized.current === true) return;
if (logging) console.log("posthog.init", apiKey);
postHogInitialized.current = true;
if (logging) console.log("Initializing PostHog");
posthog.init(apiKey, {
api_host: "https://app.posthog.com",
opt_in_site_apps: true,
debug: logging,
debug,
loaded: function (posthog) {
if (logging) console.log("posthog.loaded", apiKey);
if (logging) console.log("PostHog loaded");
if (user !== undefined) {
if (logging) console.log("posthog.identify", user.id, user.email);
if (logging) console.log("Loaded: Identifying user", user);
posthog.identify(user.id, { email: user.email });
}
},
});
postHogInitialized.current = true;
}, [apiKey, logging, user]);
//identify the user, and unidentify on log out
useEffect(() => {
useUserChanged((user) => {
if (postHogInitialized.current === false) return;
if (user === undefined) {
if (logging) console.log("posthog.reset");
posthog.reset();
} else {
if (logging) console.log("posthog.identify", user.id, user.email);
if (logging) console.log("User changed");
if (user) {
if (logging) console.log("Identifying user", user);
posthog.identify(user.id, { email: user.email });
} else {
if (logging) console.log("Resetting user");
posthog.reset();
}
}, [logging, user]);
});
//identify the organization
useEffect(() => {
useOrganizationChanged((org) => {
if (postHogInitialized.current === false) return;
if (currentOrganizationId !== undefined) {
if (logging) console.log("posthog.organization", currentOrganizationId);
posthog.group("organization", currentOrganizationId);
if (org) {
if (logging) console.log(`Grouping by organization`, org);
posthog.group("organization", org.id);
} else {
//reset the groups when you go to one of the top-level pages
if (logging) console.log("Resetting groups");
posthog.resetGroups();
}
}, [currentOrganizationId, logging]);
});
useProjectChanged((project) => {
if (postHogInitialized.current === false) return;
if (project) {
if (logging) console.log(`Grouping by project`, project);
posthog.group("project", project.id);
}
});
useJobChanged((job) => {
if (postHogInitialized.current === false) return;
if (job) {
if (logging) console.log(`Grouping by job`, job);
posthog.group("job", job.id);
}
});
//page view
useEffect(() => {
if (postHogInitialized.current === false) return;
if (logging) console.log("posthog.capture", "$pageview", location.pathname);
posthog.capture("$pageview");
}, [location, logging]);
};
+7
View File
@@ -4,6 +4,7 @@ import {
} from "remix-typedjson";
import invariant from "tiny-invariant";
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
import { useChanged } from "./useChanged";
export type MatchedProject = UseDataFunctionReturn<typeof loader>["project"];
@@ -22,3 +23,9 @@ export function useProject() {
invariant(project, "Project must be defined");
return project;
}
export const useProjectChanged = (
action: (org: MatchedProject | undefined) => void
) => {
useChanged(useOptionalProject, action);
};
+5
View File
@@ -1,5 +1,6 @@
import type { User } from "~/models/user.server";
import { useMatchesData } from "~/utils";
import { useChanged } from "./useChanged";
function isUser(user: any): user is User {
return user && typeof user === "object" && typeof user.email === "string";
@@ -22,3 +23,7 @@ export function useUser(): User {
}
return maybeUser;
}
export function useUserChanged(callback: (user: User | undefined) => void) {
useChanged(useOptionalUser, callback);
}
+5
View File
@@ -11,3 +11,8 @@ interface Array<T> {
thisArg?: any
): Exclude<S, Falsy>[];
}
// See this for more: https://twitter.com/mattpocockuk/status/1653403198885904387?s=20
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
+22
View File
@@ -0,0 +1,22 @@
import { prisma } from "~/db.server";
import { Prettify } from "~/lib.es5";
export type ExtendedEndpoint = Prettify<
Awaited<ReturnType<typeof findEndpoint>>
>;
export async function findEndpoint(id: string) {
return await prisma.endpoint.findUniqueOrThrow({
where: {
id,
},
include: {
environment: {
include: {
project: true,
organization: true,
},
},
},
});
}
@@ -116,10 +116,6 @@ export async function createOrganization(
userId,
});
await workerQueue.enqueue("organizationCreated", {
id: organization.id,
});
return { ...organization, projects: [project] };
}
@@ -132,11 +128,14 @@ export async function createEnvironment(
) {
const slug = envSlug(type);
const apiKey = createApiKeyForEnv(type);
const pkApiKey = createPkApiKeyForEnv(type);
return await prismaClient.runtimeEnvironment.create({
data: {
slug,
apiKey,
pkApiKey,
autoEnableInternalSources: type !== "DEVELOPMENT",
organization: {
connect: {
id: organization.id,
@@ -157,6 +156,10 @@ function createApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
return `tr_${envSlug(envType)}_${apiKeyId(12)}`;
}
function createPkApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
return `pk_${envSlug(envType)}_${apiKeyId(16)}`;
}
function envSlug(environmentType: RuntimeEnvironment["type"]) {
switch (environmentType) {
case "DEVELOPMENT": {
+19
View File
@@ -128,3 +128,22 @@ export function updateUser({
data: { name, email, marketingEmails, confirmedBasicDetails: true },
});
}
export async function grantUserCloudAccess({
id,
inviteCode,
}: {
id: string;
inviteCode: string;
}) {
return prisma.user.update({
where: { id },
data: {
invitationCode: {
connect: {
code: inviteCode,
},
},
},
});
}
@@ -3,7 +3,12 @@ import { PrismaClient, prisma } from "~/db.server";
import { env } from "~/env.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { OAuthClientSchema } from "~/services/externalApis/types";
import { integrationCatalog } from "~/services/externalApis/integrationCatalog.server";
import {
Help,
HelpSchema,
OAuthClientSchema,
} from "~/services/externalApis/types";
import { getSecretStore } from "~/services/secrets/secretStore.server";
export class IntegrationClientPresenter {
@@ -31,14 +36,18 @@ export class IntegrationClientPresenter {
slug: true,
authMethod: {
select: {
key: true,
type: true,
name: true,
help: true,
},
},
authSource: true,
definition: {
select: {
id: true,
name: true,
packageName: true,
},
},
connectionType: true,
@@ -93,6 +102,10 @@ export class IntegrationClientPresenter {
clientId = clientConfig?.id;
}
const help = integration.authMethod?.help
? HelpSchema.parse(integration.authMethod?.help)
: undefined;
return {
id: integration.id,
title: integration.title ?? integration.slug,
@@ -105,11 +118,13 @@ export class IntegrationClientPresenter {
integration: {
identifier: integration.definition.id,
name: integration.definition.name,
packageName: integration.definition.packageName,
},
authMethod: {
type: integration.authMethod?.type ?? "local",
name: integration.authMethod?.name ?? "Local Auth",
},
help,
};
}
}
@@ -57,6 +57,7 @@ export class ProjectPresenter {
select: {
slug: true,
definition: true,
setupStatus: true,
},
},
},
@@ -169,6 +170,7 @@ export class ProjectPresenter {
key: integration.key,
title: integration.integration.slug,
icon: integration.integration.definition.id,
setupStatus: integration.integration.setupStatus,
})
);
@@ -197,6 +199,9 @@ export class ProjectPresenter {
source: eventSpecification.source,
},
integrations,
hasIntegrationsRequiringAction: integrations.some(
(i) => i.setupStatus === "MISSING_FIELDS"
),
lastRun,
properties,
};
@@ -1,16 +1,9 @@
import { User } from "@trigger.dev/database";
import { replacements } from "@trigger.dev/internal";
import { PrismaClient, prisma } from "~/db.server";
import { env } from "~/env.server";
import { Job } from "~/models/job.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
import {
ConnectionMetadataSchema,
OAuthClientSchema,
} from "~/services/externalApis/types";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { replacements } from "@trigger.dev/internal";
export class TestJobPresenter {
#prismaClient: PrismaClient;
@@ -84,6 +77,9 @@ export class TestJobPresenter {
},
},
},
project: {
slug: projectSlug,
},
slug: jobSlug,
},
});
+50 -62
View File
@@ -7,8 +7,6 @@ import {
Outlet,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
useRouteError,
} from "@remix-run/react";
import {
TypedMetaFunction,
@@ -18,26 +16,27 @@ import {
import type { ToastMessage } from "~/models/message.server";
import { commitSession, getSession } from "~/models/message.server";
import tailwindStylesheetUrl from "~/tailwind.css";
import { RouteErrorDisplay } from "./components/ErrorDisplay";
import { HighlightInit } from "./components/HighlightInit";
import {
AppContainer,
MainCenteredContainer,
} from "./components/layout/AppLayout";
import { LinkButton } from "./components/primitives/Buttons";
import { Header1, Header3 } from "./components/primitives/Headers";
import { Toast } from "./components/primitives/Toast";
import { env } from "./env.server";
import { featuresForRequest } from "./features.server";
import { usePostHog } from "./hooks/usePostHog";
import { getUser } from "./services/session.server";
import { appEnvTitleTag } from "./utils";
import { friendlyErrorDisplay } from "./utils/httpErrors";
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
import { useHighlight } from "./hooks/useHighlight";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
};
export const meta: TypedMetaFunction<typeof loader> = ({ data }) => ({
title: `Trigger.dev${appEnvTitleTag(data.appEnv)}`,
title: `Trigger.dev${appEnvTitleTag(data?.appEnv)}`,
charset: "utf-8",
viewport: "width=device-width,initial-scale=1",
});
@@ -46,6 +45,7 @@ export const loader = async ({ request }: LoaderArgs) => {
const session = await getSession(request.headers.get("cookie"));
const toastMessage = session.get("toastMessage") as ToastMessage;
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
const highlightProjectId = env.HIGHLIGHT_PROJECT_ID;
const features = featuresForRequest(request);
return typedjson(
@@ -53,6 +53,7 @@ export const loader = async ({ request }: LoaderArgs) => {
user: await getUser(request),
toastMessage,
posthogProjectKey,
highlightProjectId,
features,
appEnv: env.APP_ENV,
appOrigin: env.APP_ORIGIN,
@@ -72,70 +73,57 @@ export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
};
export function ErrorBoundary() {
const error = useRouteError();
return (
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full overflow-hidden bg-darkBackground">
<AppContainer showBackgroundGradient={true}>
<MainCenteredContainer>
{isRouteErrorResponse(error) ? (
<ErrorDisplay
title={
friendlyErrorDisplay(error.status, error.statusText).title
}
message={
error.data.message ??
friendlyErrorDisplay(error.status, error.statusText).message
}
/>
) : error instanceof Error ? (
<ErrorDisplay title={error.name} message={error.message} />
) : (
<ErrorDisplay title="Oops" message={JSON.stringify(error)} />
)}
</MainCenteredContainer>
</AppContainer>
<Scripts />
</body>
</html>
);
}
function ErrorDisplay({ title, message }: { title: string; message?: string }) {
return (
<div className="p-4">
<Header1 className="mb-4 border-b border-slate-800 pb-4">{title}</Header1>
{message && <Header3>{message}</Header3>}
<LinkButton to="/" variant="primary/medium" className="mt-8">
Home
</LinkButton>
</div>
<>
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full overflow-hidden bg-darkBackground">
<AppContainer showBackgroundGradient={true}>
<MainCenteredContainer>
<RouteErrorDisplay />
</MainCenteredContainer>
</AppContainer>
<Scripts />
</body>
</html>
</>
);
}
function App() {
const { posthogProjectKey } = useTypedLoaderData<typeof loader>();
const { posthogProjectKey, highlightProjectId } =
useTypedLoaderData<typeof loader>();
usePostHog(posthogProjectKey);
useHighlight();
return (
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full overflow-hidden bg-darkBackground">
<Outlet />
<Toast />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
<>
{highlightProjectId && (
<HighlightInit
projectId={highlightProjectId}
tracingOrigins={true}
networkRecording={{ enabled: true, recordHeadersAndBody: true }}
/>
)}
<html lang="en" className="h-full">
<head>
<Meta />
<Links />
</head>
<body className="h-full overflow-hidden bg-darkBackground">
<HighlightErrorBoundary>
<Outlet />
</HighlightErrorBoundary>
<Toast />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
</>
);
}
@@ -21,6 +21,7 @@ import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import useWindowSize from "react-use/lib/useWindowSize";
import { docsPath, projectIntegrationsPath } from "~/utils/pathBuilder";
export const handle: Handle = {
breadcrumb: {
@@ -77,11 +78,24 @@ export default function Page() {
{(open) => (
<div
className={cn(
"grid h-full gap-4",
"grid h-fit gap-4",
open ? "grid-cols-2" : "grid-cols-1"
)}
>
<div>
{project.jobs.length > 0 &&
project.jobs.some(
(j) => j.hasIntegrationsRequiringAction
) && (
<Callout
variant="error"
to={projectIntegrationsPath(organization, project)}
className="mb-2"
>
Some of your Jobs have Integrations that have not been
configured.
</Callout>
)}
<div className="mb-2 flex items-center justify-between gap-x-2">
{project.jobs.length === 0 ? (
<Header2>Jobs</Header2>
@@ -117,7 +131,7 @@ export default function Page() {
{project.jobs.length === 1 ? (
<Callout
variant="docs"
href="https://trigger.dev/docs/documentation/quickstart#your-first-job"
to={docsPath("documentation/quickstart#your-first-job")}
className="my-3"
>
Create your first Job in code
@@ -125,7 +139,7 @@ export default function Page() {
) : (
<Callout
variant="docs"
href="https://trigger.dev/docs/documentation/guides/create-a-job"
to={docsPath("documentation/guides/create-a-job")}
className="my-3"
>
Create another Job
@@ -1,5 +1,20 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher, useRevalidator } from "@remix-run/react";
import { useEffect } from "react";
import { useEventSource } from "remix-utils";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { Button } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { FormError } from "~/components/primitives/FormError";
import { Header1, Header2 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Sheet,
SheetBody,
@@ -7,25 +22,9 @@ import {
SheetHeader,
} from "~/components/primitives/Sheet";
import { ClientEndpoint } from "~/presenters/EnvironmentsPresenter.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { useFetcher, useRevalidator } from "@remix-run/react";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Input } from "~/components/primitives/Input";
import { Button } from "~/components/primitives/Buttons";
import { Label } from "~/components/primitives/Label";
import { Hint } from "~/components/primitives/Hint";
import { InlineCode } from "~/components/code/InlineCode";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Callout } from "~/components/primitives/Callout";
import { formatDateTime } from "~/utils";
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
import { FormError } from "~/components/primitives/FormError";
import { useEventSource } from "remix-utils";
import { useEffect } from "react";
import { endpointStreamingPath } from "~/utils/pathBuilder";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
type ConfigureEndpointSheetProps = {
slug: string;
@@ -147,9 +146,11 @@ export function ConfigureEndpointSheet({
>
<Paragraph variant="small" className="grow text-green-200">
Endpoint configured. Last refreshed:{" "}
{endpoint.latestIndex
? formatDateTime(endpoint.latestIndex.updatedAt, "long")
: ""}
{endpoint.latestIndex ? (
<DateTime date={endpoint.latestIndex.updatedAt} />
) : (
""
)}
</Paragraph>
<Button
variant="primary/small"
@@ -1,13 +1,14 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { useMemo, useState } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import {
EnvironmentLabel,
environmentTitle,
} from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { ButtonContent } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header1, Header2 } from "~/components/primitives/Headers";
import {
PageDescription,
@@ -30,9 +31,9 @@ import {
EnvironmentsPresenter,
} from "~/presenters/EnvironmentsPresenter.server";
import { requireUserId } from "~/services/session.server";
import { formatDateTime, requestUrl } from "~/utils";
import { Handle } from "~/utils/handle";
import { ProjectParamSchema } from "~/utils/pathBuilder";
import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
@@ -118,7 +119,7 @@ export default function Page() {
))}
</div>
<Header1 className="mb-2">Clients</Header1>
<Header1 className="mb-2">Endpoints</Header1>
<div className="flex flex-col gap-4">
{clients.length > 0 ? (
clients.map((client) => (
@@ -128,9 +129,9 @@ export default function Page() {
<TableHeader>
<TableRow>
<TableHeaderCell>Environment</TableHeaderCell>
<TableHeaderCell>Url</TableHeaderCell>
<TableHeaderCell>Last refreshed</TableHeaderCell>
<TableHeaderCell>Jobs</TableHeaderCell>
<TableHeaderCell>Url</TableHeaderCell>
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
</TableRow>
</TableHeader>
@@ -196,7 +197,7 @@ function EndpointRow({
</TableCell>
<TableCell onClick={onClick} colSpan={4} alignment="right">
<div className="flex items-center justify-end gap-4">
<span className="text-rose-500">
<span className="text-amber-500">
The {environmentTitle({ type })} environment is not configured
</span>
<ButtonContent variant="primary/small">Configure</ButtonContent>
@@ -212,15 +213,17 @@ function EndpointRow({
<EnvironmentLabel environment={{ type }} />
</div>
</TableCell>
<TableCell onClick={onClick}>{endpoint.url}</TableCell>
<TableCell onClick={onClick}>
{endpoint.latestIndex
? formatDateTime(endpoint.latestIndex.updatedAt)
: ""}
{endpoint.latestIndex ? (
<DateTime date={endpoint.latestIndex.updatedAt} />
) : (
""
)}
</TableCell>
<TableCell onClick={onClick}>
{endpoint.latestIndex?.stats.jobs ?? ""}
</TableCell>
<TableCell onClick={onClick}>{endpoint.url}</TableCell>
<TableCellChevron onClick={onClick} />
</TableRow>
);
@@ -10,6 +10,7 @@ import { NoIntegrationSheet } from "~/components/integrations/NoIntegrationSheet
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { DateTime } from "~/components/primitives/DateTime";
import { Header2 } from "~/components/primitives/Headers";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Input } from "~/components/primitives/Input";
@@ -45,7 +46,6 @@ import {
IntegrationsPresenter,
} from "~/presenters/IntegrationsPresenter.server";
import { requireUser } from "~/services/session.server";
import { formatDateTime } from "~/utils";
import { Handle } from "~/utils/handle";
import {
ProjectParamSchema,
@@ -351,7 +351,10 @@ function ConnectedIntegrationsList({
: client.connectionsCount}
</TableCell>
<TableCell to={path}>
{formatDateTime(client.createdAt, "medium")}
<DateTime
date={client.createdAt}
includeSeconds={false}
/>
</TableCell>
<TableCellChevron to={path} />
</TableRow>
@@ -387,7 +390,7 @@ function IntegrationsWithMissingFields({
<div className="mb-6">
<Header2 className="mb-2 flex items-center gap-1">
<NamedIcon name="error" className="h-5 w-5" />
Integrations with missing details
Integrations requiring configuration
</Header2>
<Table>
@@ -448,7 +451,7 @@ function IntegrationsWithMissingFields({
<IntegrationWithMissingFieldSheet
integration={integration}
organizationId={organizationId}
button={formatDateTime(client.createdAt, "medium")}
button={<DateTime date={client.createdAt} />}
callbackUrl={callbackUrl}
existingIntegration={client}
className="flex w-full cursor-pointer justify-start"
@@ -1,8 +1,10 @@
import { useMatches } from "@remix-run/react";
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { HowToUseThisIntegration } from "~/components/helpContent/HelpContentText";
import { JobSkeleton } from "~/components/jobs/JobSkeleton";
import { JobsTable } from "~/components/jobs/JobsTable";
import { Callout } from "~/components/primitives/Callout";
import { Header2 } from "~/components/primitives/Headers";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Input } from "~/components/primitives/Input";
@@ -13,7 +15,11 @@ import { IntegrationClientJobsPresenter } from "~/presenters/IntegrationClientJo
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { IntegrationClientParamSchema } from "~/utils/pathBuilder";
import {
IntegrationClientParamSchema,
docsIntegrationPath,
docsRoot,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -90,7 +96,22 @@ export default function Page() {
)}
</div>
<HelpContent title="How to use this Integration">
<HowToUseThisIntegration />
<HowToUseThisIntegration
integration={{
name: client.integration.name,
identifier: client.integrationIdentifier,
packageName: client.integration.packageName,
}}
integrationClient={client}
help={client.help}
/>
<Callout
variant="docs"
to={docsIntegrationPath(client.integration.identifier)}
>
View the docs to learn more about using the{" "}
{client.integration.name} Integration.
</Callout>
</HelpContent>
</div>
)}
@@ -1,6 +1,7 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { connectionType } from "~/components/integrations/connectionType";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Table,
@@ -11,11 +12,8 @@ import {
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { useIntegrationClient } from "~/hooks/useIntegrationClient";
import { useProject } from "~/hooks/useProject";
import { IntegrationClientConnectionsPresenter } from "~/presenters/IntegrationClientConnectionsPresenter.server";
import { requireUserId } from "~/services/session.server";
import { formatDateTime } from "~/utils";
import { Handle } from "~/utils/handle";
import { IntegrationClientParamSchema } from "~/utils/pathBuilder";
@@ -43,8 +41,6 @@ export const handle: Handle = {
export default function Page() {
const { connections } = useTypedLoaderData<typeof loader>();
const client = useIntegrationClient();
const project = useProject();
return (
<Table>
@@ -71,8 +67,12 @@ export default function Page() {
<TableCell>
<ExpiresAt expiresAt={connection.expiresAt} />
</TableCell>
<TableCell>{formatDateTime(connection.createdAt)}</TableCell>
<TableCell>{formatDateTime(connection.updatedAt)}</TableCell>
<TableCell>
{<DateTime date={connection.createdAt} />}
</TableCell>
<TableCell>
{<DateTime date={connection.updatedAt} />}
</TableCell>
</TableRow>
);
})
@@ -98,7 +98,7 @@ function ExpiresAt({ expiresAt }: { expiresAt: Date | null }) {
return (
<span className={inPast ? "text-rose-500" : ""}>
{formatDateTime(expiresAt)}
<DateTime date={expiresAt} />
</span>
);
}
@@ -1,9 +1,10 @@
import { Outlet } from "@remix-run/react";
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { connectionType } from "~/components/integrations/connectionType";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import {
PageHeader,
PageInfoGroup,
@@ -17,7 +18,6 @@ import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { IntegrationClientPresenter } from "~/presenters/IntegrationClientPresenter.server";
import { requireUser } from "~/services/session.server";
import { formatDateTime } from "~/utils";
import { Handle } from "~/utils/handle";
import {
IntegrationClientParamSchema,
@@ -57,6 +57,7 @@ export default function Integrations() {
const { client } = useTypedLoaderData<typeof loader>();
const organization = useOrganization();
const project = useProject();
let tabs = [
{
label: "Jobs",
@@ -114,7 +115,7 @@ export default function Integrations() {
<PageInfoProperty
icon="calendar"
label="Added"
value={formatDateTime(client.createdAt)}
value={<DateTime date={client.createdAt} />}
/>
</PageInfoGroup>
</PageInfoRow>
@@ -24,10 +24,11 @@ function NextButton({ cursor }: { cursor?: string }) {
return path ? (
<LinkButton
to={path}
variant={"secondary/small"}
variant={"tertiary/small"}
TrailingIcon="chevron-right"
className="flex items-center"
>
<span className="sr-only">Next</span>
Next
</LinkButton>
) : null;
}
@@ -38,10 +39,11 @@ function PreviousButton({ cursor }: { cursor?: string }) {
return path ? (
<LinkButton
to={path}
variant={"secondary/small"}
variant={"tertiary/small"}
LeadingIcon="chevron-left"
className="flex items-center"
>
<span className="sr-only">Previous</span>
Prev
</LinkButton>
) : null;
}
@@ -1,14 +1,21 @@
import { useNavigation } from "@remix-run/react";
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
import { z } from "zod";
import { HowToRunYourJob } from "~/components/helpContent/HelpContentText";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { RunsTable } from "~/components/runs/RunsTable";
import { RunListPresenter } from "~/presenters/RunListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { JobParamsSchema, projectIntegrationsPath } from "~/utils/pathBuilder";
import { ListPagination } from "./ListPagination";
import { useNavigation } from "@remix-run/react";
import { JobParamsSchema } from "~/utils/pathBuilder";
import { Callout } from "~/components/primitives/Callout";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useJob } from "~/hooks/useJob";
import simplur from "simplur";
export const DirectionSchema = z.union([
z.literal("forward"),
@@ -54,17 +61,51 @@ export default function Page() {
const { list } = useTypedLoaderData<typeof loader>();
const navigation = useNavigation();
const isLoading = navigation.state !== "idle";
const organization = useOrganization();
const project = useProject();
const job = useJob();
return (
<div>
<ListPagination list={list} className="mb-1 justify-end" />
<RunsTable
total={list.runs.length}
hasFilters={false}
runs={list.runs}
isLoading={isLoading}
/>
<ListPagination list={list} className="mb-1 justify-end" />
</div>
<>
{job.hasIntegrationsRequiringAction && (
<Callout
variant="error"
to={projectIntegrationsPath(organization, project)}
className="mb-2"
>
{simplur`This Job has ${
job.integrations.filter((j) => j.setupStatus === "MISSING_FIELDS")
.length
} Integration[|s] that [has|have] not been configured.`}
</Callout>
)}
<Help defaultOpen={list.runs.length === 0}>
{(open) => (
<div
className={cn(
"grid h-fit gap-4",
open ? "grid-cols-2" : "grid-cols-1"
)}
>
<div>
<div className="mb-2 flex items-center justify-end gap-x-2">
<ListPagination list={list} />
<HelpTrigger title="How do I run my Job?" />
</div>
<RunsTable
total={list.runs.length}
hasFilters={false}
runs={list.runs}
isLoading={isLoading}
/>
<ListPagination list={list} className="mt-2 justify-end" />
</div>
<HelpContent title="How to run your Job">
<HowToRunYourJob />
</HelpContent>
</div>
)}
</Help>
</>
);
}
@@ -1,8 +1,9 @@
import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RunStatusIcon, RunStatusLabel } from "~/components/runs/RunStatuses";
import { useRun } from "~/hooks/useRun";
import { formatDateTime, formatDuration } from "~/utils";
import { formatDuration } from "~/utils";
import {
RunPanel,
RunPanelBody,
@@ -28,14 +29,14 @@ export default function RunCompletedPage() {
<RunPanelIconProperty
icon="calendar"
label="Started at"
value={formatDateTime(run.startedAt, "long")}
value={<DateTime date={run.startedAt} />}
/>
)}
{run.completedAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={formatDateTime(run.completedAt, "long")}
value={<DateTime date={run.completedAt} />}
/>
)}
{run.startedAt && run.completedAt && (
@@ -1,7 +1,9 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Header3 } from "~/components/primitives/Headers";
import { useLocales } from "~/components/primitives/LocaleProvider";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Table,
@@ -12,8 +14,9 @@ import {
TableRow,
} from "~/components/primitives/Table";
import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer";
import { requireUserId } from "~/services/session.server";
import { formatDateTime, formatDuration } from "~/utils";
import { formatDuration } from "~/utils";
import { cn } from "~/utils/cn";
import { TaskParamsSchema } from "~/utils/pathBuilder";
import {
@@ -31,7 +34,6 @@ import {
} from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/RunCard";
import { TaskStatusIcon } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/TaskStatus";
import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -56,6 +58,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
export default function Page() {
const { task } = useTypedLoaderData<typeof loader>();
const locales = useLocales();
const {
name,
@@ -98,14 +101,14 @@ export default function Page() {
<RunPanelIconProperty
icon="calendar"
label="Started at"
value={formatDateTime(startedAt, "long")}
value={<DateTime date={startedAt} />}
/>
)}
{completedAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={formatDateTime(completedAt, "long")}
value={<DateTime date={completedAt} />}
/>
)}
{delayUntil && !completedAt && (
@@ -113,7 +116,7 @@ export default function Page() {
<RunPanelIconProperty
icon="flag"
label="Continues at"
value={formatDateTime(delayUntil, "long")}
value={<DateTime date={delayUntil} />}
/>
<UpdatingDelay delayUntil={delayUntil} />
</>
@@ -160,9 +163,13 @@ export default function Page() {
<TaskAttemptStatusLabel status={attempt.status} />
</TableCell>
<TableCell>
{attempt.status === "PENDING" && attempt.runAt
? formatDateTime(attempt.runAt, "long")
: formatDateTime(attempt.updatedAt, "long")}
<DateTime
date={
attempt.status === "PENDING" && attempt.runAt
? attempt.runAt
: attempt.updatedAt
}
/>
</TableCell>
<TableCell>{attempt.error}</TableCell>
</TableRow>
@@ -1,11 +1,11 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Header3 } from "~/components/primitives/Headers";
import { useJob } from "~/hooks/useJob";
import { useRun } from "~/hooks/useRun";
import { EventDetailsPresenter } from "~/presenters/EventDetailsPresenter.server";
import { formatDateTime } from "~/utils";
import { RunParamsSchema } from "~/utils/pathBuilder";
import {
RunPanel,
@@ -49,13 +49,13 @@ export default function Page() {
<RunPanelIconProperty
icon="calendar"
label="Created"
value={formatDateTime(timestamp, "long")}
value={<DateTime date={timestamp} />}
/>
{deliveredAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={formatDateTime(deliveredAt, "long")}
value={<DateTime date={deliveredAt} />}
/>
)}
<RunPanelIconProperty icon="id" label="Event name" value={name} />
@@ -173,7 +173,7 @@ export function RunPanelIconProperty({
}: {
icon: ReactNode;
label: string;
value: string;
value: ReactNode;
}) {
return (
<div className="flex items-center gap-2">
@@ -1,9 +1,9 @@
import type { RuntimeEnvironmentType } from "@trigger.dev/internal";
import { conform } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { BoltIcon, ForwardIcon } from "@heroicons/react/24/solid";
import { Form, Outlet, useNavigate, useRevalidator } from "@remix-run/react";
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
import type { RuntimeEnvironmentType } from "@trigger.dev/internal";
import { useEffect, useMemo } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils";
@@ -13,6 +13,7 @@ import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Button } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { DateTime } from "~/components/primitives/DateTime";
import { Header2 } from "~/components/primitives/Headers";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import {
@@ -42,12 +43,15 @@ import { useOrganization } from "~/hooks/useOrganizations";
import { usePathName } from "~/hooks/usePathName";
import { useProject } from "~/hooks/useProject";
import { JobRunStatus } from "~/models/job.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import {
redirectBackWithErrorMessage,
redirectWithSuccessMessage,
} from "~/models/message.server";
import { RunPresenter } from "~/presenters/RunPresenter.server";
import { ContinueRunService } from "~/services/runs/continueRun.server";
import { ReRunService } from "~/services/runs/reRun.server";
import { requireUserId } from "~/services/session.server";
import { formatDateTime, formatDuration } from "~/utils";
import { formatDuration } from "~/utils";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import {
@@ -112,6 +116,10 @@ export const action: ActionFunction = async ({ request, params }) => {
const rerunService = new ReRunService();
const run = await rerunService.call({ runId: runParam });
if (!run) {
return redirectBackWithErrorMessage(request, "Unable to retry run");
}
return redirectWithSuccessMessage(
runDashboardPath(
{ slug: organizationSlug },
@@ -226,9 +234,11 @@ export default function Page() {
icon={"calendar"}
label={"Started"}
value={
run.startedAt
? formatDateTime(run.startedAt)
: "Not started yet"
run.startedAt ? (
<DateTime date={run.startedAt} />
) : (
"Not started yet"
)
}
/>
<PageInfoProperty
@@ -333,14 +343,14 @@ export default function Page() {
<RunPanelIconProperty
icon="calendar"
label="Started at"
value={formatDateTime(run.startedAt, "long")}
value={<DateTime date={run.startedAt} />}
/>
)}
{run.completedAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={formatDateTime(run.completedAt, "long")}
value={<DateTime date={run.completedAt} />}
/>
)}
{run.startedAt && run.completedAt && (
@@ -373,7 +383,7 @@ export default function Page() {
) : (
run.output === null && (
<Paragraph variant="small">
This run returned nothing
This Run returned nothing.
</Paragraph>
)
)}
@@ -4,7 +4,7 @@ export default function Page() {
return (
<ComingSoon
title="Job settings"
description="Disable a job, archive it and more."
description="Disable a Job, archive it and more."
icon="settings"
/>
);
@@ -12,10 +12,7 @@ import { HowToRunATest } from "~/components/helpContent/HelpContentText";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { FormError } from "~/components/primitives/FormError";
import { HelpTrigger } from "~/components/primitives/Help";
import { HelpContent } from "~/components/primitives/Help";
import { Help } from "~/components/primitives/Help";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Popover, PopoverContent } from "~/components/primitives/Popover";
import {
Select,
@@ -25,18 +22,16 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { redirectWithSuccessMessage } from "~/models/message.server";
import {
redirectBackWithErrorMessage,
redirectWithSuccessMessage,
} from "~/models/message.server";
import { TestJobPresenter } from "~/presenters/TestJobPresenter.server";
import { TestJobService } from "~/services/jobs/testJob.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { formDataAsObject } from "~/utils/formData";
import { Handle } from "~/utils/handle";
import {
JobParamsSchema,
jobTestPath,
runDashboardPath,
} from "~/utils/pathBuilder";
import { JobParamsSchema, runDashboardPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -81,7 +76,6 @@ const schema = z.object({
//todo save the chosen environment to a cookie (for that user), use it to default the env dropdown
export const action: ActionFunction = async ({ request, params }) => {
const userId = await requireUserId(request);
const { organizationSlug, projectParam, jobParam } =
JobParamsSchema.parse(params);
@@ -100,6 +94,13 @@ export const action: ActionFunction = async ({ request, params }) => {
versionId: submission.value.versionId,
});
if (!run) {
return redirectBackWithErrorMessage(
request,
"Unable to start a test run: Something went wrong"
);
}
return redirectWithSuccessMessage(
runDashboardPath(
{ slug: organizationSlug },
@@ -177,7 +178,7 @@ export default function Page() {
}
return (
<Help defaultOpen={!hasTestRuns}>
<Help defaultOpen={true}>
{(open) => (
<div
className={cn(
@@ -253,7 +254,7 @@ export default function Page() {
</Popover>
)}
</div>
<HelpTrigger title="How do I run a test" />
<HelpTrigger title="How do I run a test?" />
</div>
<div className="flex-1 overflow-auto rounded border border-slate-850 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<JSONEditor
@@ -4,7 +4,7 @@ export default function Page() {
return (
<ComingSoon
title="View all Triggers"
description="View all of the registered Triggers for this job."
description="View all of the registered Triggers for this Job."
icon="trigger"
/>
);
@@ -43,7 +43,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
});
if (job === null) {
throw new Response("Not Found", { status: 404 });
throw new Response("Not Found", {
status: 404,
statusText: `There is no Job ${jobParam} in this Project.`,
});
}
//todo identify job
@@ -111,7 +114,7 @@ export default function Job() {
<PageInfoProperty
label="Integrations"
value={
<div className="flex gap-0.5">
<span className="flex gap-0.5">
{job.integrations.map((integration, index) => (
<NamedIcon
key={index}
@@ -119,7 +122,7 @@ export default function Job() {
className={"h-4 w-4"}
/>
))}
</div>
</span>
}
/>
)}
@@ -2,14 +2,18 @@ import { Outlet } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import invariant from "tiny-invariant";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import {
ProjectSideMenu,
SideMenuContainer,
} from "~/components/navigation/ProjectSideMenu";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { ProjectPresenter } from "~/presenters/ProjectPresenter.server";
import { analytics } from "~/services/analytics.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { projectPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -25,7 +29,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
});
if (!project) {
throw new Response("Not Found", { status: 404 });
throw new Response("Not Found", {
status: 404,
statusText: `Project ${projectParam} not found in your Organization.`,
});
}
analytics.project.identify({ project });
@@ -34,6 +41,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
project,
});
} catch (error) {
if (error instanceof Response) {
throw error;
}
console.error(error);
throw new Response(undefined, {
status: 400,
@@ -61,3 +72,13 @@ export default function Project() {
</>
);
}
export function ErrorBoundary() {
const org = useOrganization();
const project = useProject();
return (
<RouteErrorDisplay
button={{ title: project.name, to: projectPath(org, project) }}
/>
);
}
@@ -1,6 +1,6 @@
import { useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { EnvelopeIcon, UserPlusIcon } from "@heroicons/react/20/solid";
import { UserPlusIcon } from "@heroicons/react/20/solid";
import { Form, useActionData } from "@remix-run/react";
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
import { useState } from "react";
@@ -29,6 +29,7 @@ import {
LinkButton,
} from "~/components/primitives/Buttons";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import { Paragraph } from "~/components/primitives/Paragraph";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { useOrganization } from "~/hooks/useOrganizations";
@@ -39,16 +40,13 @@ import {
} from "~/models/member.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { requireUserId } from "~/services/session.server";
import { formatDateTime, titleCase } from "~/utils";
import {
inviteTeamMemberPath,
organizationTeamPath,
resendInvitePath,
} from "~/utils/pathBuilder";
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
import { resendSchema } from "../invite-resend";
import { EnvelopeOpenIcon } from "@heroicons/react/24/solid";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import { DateTime } from "~/components/primitives/DateTime";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -169,7 +167,7 @@ export default function Page() {
<div className="flex flex-col gap-0.5">
<Header3>{invite.email}</Header3>
<Paragraph variant="small">
Invite sent {formatDateTime(invite.updatedAt, "medium")}
Invite sent {<DateTime date={invite.updatedAt} />}
</Paragraph>
</div>
<div className="flex grow items-center justify-end gap-4">
@@ -3,6 +3,8 @@ import { Outlet } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import invariant from "tiny-invariant";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import { useOrganization } from "~/hooks/useOrganizations";
import { getOrganizationFromSlug } from "~/models/organization.server";
import { analytics } from "~/services/analytics.server";
import {
@@ -10,6 +12,7 @@ import {
setCurrentOrg,
} from "~/services/currentOrganization.server";
import { requireUserId } from "~/services/session.server";
import { organizationPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -49,6 +52,15 @@ export default function Organization() {
);
}
export function ErrorBoundary() {
const org = useOrganization();
return (
<RouteErrorDisplay
button={{ title: org.title, to: organizationPath(org) }}
/>
);
}
export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
if (options.formAction === "/resources/environment") {
return false;
+34 -2
View File
@@ -1,22 +1,41 @@
import { Outlet, ShouldRevalidateFunction } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import { ImpersonationBanner } from "~/components/ImpersonationBanner";
import { NoMobileOverlay } from "~/components/NoMobileOverlay";
import { AppContainer } from "~/components/layout/AppLayout";
import {
AppContainer,
MainCenteredContainer,
} from "~/components/layout/AppLayout";
import { NavBar } from "~/components/navigation/NavBar";
import { featuresForRequest } from "~/features.server";
import { useIsProjectChildPage } from "~/hooks/useIsProjectChildPage";
import { getOrganizations } from "~/models/organization.server";
import { getImpersonationId } from "~/services/impersonation.server";
import { clearRedirectTo, commitSession } from "~/services/redirectTo.server";
import { requireUser } from "~/services/session.server";
import { confirmBasicDetailsPath } from "~/utils/pathBuilder";
import {
confirmBasicDetailsPath,
invitationCodePath,
} from "~/utils/pathBuilder";
export const loader = async ({ request }: LoaderArgs) => {
const user = await requireUser(request);
const organizations = await getOrganizations({ userId: user.id });
const impersonationId = await getImpersonationId(request);
const features = featuresForRequest(request);
const url = new URL(request.url);
if (
features.isManagedCloud &&
!user.invitationCodeId &&
url.pathname !== invitationCodePath()
) {
throw redirect(invitationCodePath());
}
//you have to confirm basic details before you can do anything
if (!user.confirmedBasicDetails) {
@@ -78,3 +97,16 @@ export default function App() {
</>
);
}
export function ErrorBoundary() {
return (
<>
<NoMobileOverlay />
<AppContainer showBackgroundGradient={true}>
<MainCenteredContainer>
<RouteErrorDisplay />
</MainCenteredContainer>
</AppContainer>
</>
);
}

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