Compare commits
321 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd2f68a51d | |||
| 64477f6b3a | |||
| 8b09dfd697 | |||
| f330673143 | |||
| 9351c05126 | |||
| af722e54d8 | |||
| 1eb89844f4 | |||
| 004423aea3 | |||
| 369c6245ab | |||
| b8ae937966 | |||
| 3b510a4f3d | |||
| b3e609d53d | |||
| 2f07a792e9 | |||
| 078bf2bc9c | |||
| a7c8f0b90e | |||
| 2c70b7a360 | |||
| c518185947 | |||
| b314178de2 | |||
| d3608be1a0 | |||
| 4c79129242 | |||
| 17e6d42989 | |||
| 0a3bbc1caf | |||
| 89e1331e49 | |||
| d934a53e6d | |||
| 2398d60b1a | |||
| 1bd755b628 | |||
| 861d5fa1b9 | |||
| 0239fc6773 | |||
| de57592d79 | |||
| f70f3a0a5d | |||
| 6c96b331dd | |||
| 767e09ee18 | |||
| b2f3ade7c7 | |||
| 8f050b40c1 | |||
| 3cf01ff14c | |||
| 73e0084125 | |||
| 093c9ec015 | |||
| 475dddfbe4 | |||
| 188e4f2740 | |||
| 4dc2b3184b | |||
| 139d541d86 | |||
| 10798c0a05 | |||
| 9c6ad11479 | |||
| 5527ef17f4 | |||
| 981f4010a9 | |||
| db7e8923c0 | |||
| 0eef5c2f01 | |||
| 08901f2e8a | |||
| 51b365226d | |||
| 0b89ee7eb5 | |||
| d7c4b24203 | |||
| d7352867ec | |||
| a2b97bd918 | |||
| 5991a767f7 | |||
| 643eba263e | |||
| 4db8a3992b | |||
| 03f7156749 | |||
| d9912e5548 | |||
| 3c85c985d9 | |||
| 0cf848b9b9 | |||
| ef4056abde | |||
| 3a6c25d42d | |||
| 642c95027c | |||
| 8a66a5c6c7 | |||
| 7dcc55a083 | |||
| 6989fec9d0 | |||
| bba190e4e9 | |||
| 6f670c8cc6 | |||
| d8626c624f | |||
| 454ad25b1d | |||
| 4dce703b20 | |||
| ce1e911c90 | |||
| f84f4dcee6 | |||
| bfa3757ee0 | |||
| 1fea5d7af3 | |||
| dd33bbf6a8 | |||
| 74640b1a82 | |||
| 51e6a74eda | |||
| 27a9812194 | |||
| ba03a9ed20 | |||
| f5c1c62a09 | |||
| 420e719db2 | |||
| c448f53964 | |||
| 72230f67d8 | |||
| 4bdc28d7fc | |||
| eb5195768b | |||
| 5b6539d799 | |||
| b1b471cf63 | |||
| bcd80463f3 | |||
| e2781cb090 | |||
| 09f906f7a1 | |||
| 706ab20ced | |||
| 1dde615686 | |||
| dda8be1fd0 | |||
| 2888877463 | |||
| ac4253a187 | |||
| a6f79d77d1 | |||
| 4ca841a1d5 | |||
| 4d2a8728be | |||
| ae3afdfc7e | |||
| e135b06e2c | |||
| 106843be09 | |||
| 022021dc4b | |||
| 8815b1608e | |||
| 3dcd7743be | |||
| 66fd838085 | |||
| b04852c98c | |||
| ad19bfa283 | |||
| ee86910caa | |||
| 9a4daaf024 | |||
| fcf1202af3 | |||
| 43c402ccae | |||
| 24334a1e78 | |||
| 37e27510de | |||
| fe35e8ea39 | |||
| db49879584 | |||
| df68305f28 | |||
| 078b10ccea | |||
| 1f8a36efb4 | |||
| b0c386ff87 | |||
| 8b69e06f79 | |||
| 24aa4c7406 | |||
| 621c45aaf1 | |||
| 9f67b83bc5 | |||
| da7dfd4bca | |||
| 07250f4ace | |||
| 8b5055d9d3 | |||
| ec36918061 | |||
| c895722ffc | |||
| 7df34db36b | |||
| 3008909f02 | |||
| 8af381729c | |||
| 8ca0aeb1bd | |||
| 99f8ec935b | |||
| d07cfe8fdb | |||
| 2f1b2a32c3 | |||
| b6fd62eb54 | |||
| 1dfe7e0ab5 | |||
| 17c7e9765e | |||
| 0ae03477df | |||
| 2348e95b6c | |||
| 439ecf4a7f | |||
| 68750da731 | |||
| 61de39712a | |||
| d32e6f9839 | |||
| 0cda4621c7 | |||
| 4bcd654e49 | |||
| bf0a0a17a0 | |||
| ca59b6b236 | |||
| 2a6cd98d4e | |||
| d4da2f322b | |||
| 04ce02bb5d | |||
| 62e3bb93c1 | |||
| 7da6b94daf | |||
| e95bd3b6fe | |||
| 8e4c70468d | |||
| 1ea5082314 | |||
| 267cbc751c | |||
| 5e522e1f3e | |||
| ba7ef25bcb | |||
| e132ea33f2 | |||
| d0b19d50e4 | |||
| 226ded6156 | |||
| 8f5abfd0a6 | |||
| 1bf2802b67 | |||
| b95826840a | |||
| 584e2b3ffb | |||
| 69af845ad1 | |||
| fba8eec319 | |||
| 01c0f9b324 | |||
| 86e8ccbbd6 | |||
| 0b45338300 | |||
| eafe6a0d7b | |||
| 3fb41f68fb | |||
| f5c819a489 | |||
| 5336317dbb | |||
| 6099e06a71 | |||
| 0066971b77 | |||
| c417a7f9ab | |||
| 1961b994d7 | |||
| 5288ca3840 | |||
| f985dc93aa | |||
| d6e43c40c9 | |||
| 184c24cace | |||
| 9861cb9aa7 | |||
| 2d722b94bd | |||
| 2db67e3b4d | |||
| 6290232858 | |||
| 7f6bf992a2 | |||
| 1f5bd8d6bb | |||
| 46383e57d2 | |||
| 2611b2c171 | |||
| 229207c8f8 | |||
| 2eb0b3abce | |||
| 8b6909b486 | |||
| dbb9fe44d8 | |||
| bb11001fb7 | |||
| b35457bb53 | |||
| 1ea0d39fa2 | |||
| 9193b3c90b | |||
| c349b50164 | |||
| fe3bc6efe4 | |||
| df138ee96e | |||
| d0b8e560c9 | |||
| 537409cec1 | |||
| bac8ad1040 | |||
| cc26cbeb7b | |||
| 3b6c2bd0e8 | |||
| 0b004fbcaf | |||
| c72c21e2a9 | |||
| d6310a79fa | |||
| 953354f367 | |||
| f8ddb21510 | |||
| 761e89108b | |||
| ec34a84d6d | |||
| 9cb5e6847b | |||
| 43f0c6f7f2 | |||
| ffcad81498 | |||
| f16a337c50 | |||
| 30e5303c36 | |||
| 19d5cb596f | |||
| ff58925606 | |||
| 12b16a0030 | |||
| b08c10a716 | |||
| b878561f65 | |||
| 5e36ad961f | |||
| 3dc2f0cfff | |||
| 073c4fb76e | |||
| cef0bd172d | |||
| 23eed197ee | |||
| 3f8f76df76 | |||
| b047023d9c | |||
| 420655f231 | |||
| 520ab88a97 | |||
| f96da50e6a | |||
| 1dc42daed4 | |||
| 0a80e2f5ec | |||
| 4661d2019c | |||
| 4e4e7b705d | |||
| bae1b0be72 | |||
| e890ee01a3 | |||
| af4b0757a9 | |||
| 6ef62d82c4 | |||
| 44df509b62 | |||
| 5d3fe2a5ea | |||
| 076e6dce73 | |||
| f745320456 | |||
| 6e64aaa1ee | |||
| 8b68edfc05 | |||
| 0a7bae4dac | |||
| b925a573f3 | |||
| 0012bb21ce | |||
| 3b3ec9fc4a | |||
| 396663d65c | |||
| f275b47755 | |||
| cfb7481d7e | |||
| 57fcce90d9 | |||
| 2c0ea0c108 | |||
| e81317b72c | |||
| 686cf9dc7e | |||
| a9e7cda37d | |||
| eae0b8a95e | |||
| 1b65b1d596 | |||
| d85b1fe2fd | |||
| f00006af1b | |||
| 75a07fe4ef | |||
| 946e429906 | |||
| 2e5a401ff7 | |||
| b8b84d8d16 | |||
| 4373538476 | |||
| af79ea2f72 | |||
| aab859bc20 | |||
| c692c16183 | |||
| cb59808be2 | |||
| 039777bf63 | |||
| 3547ec13c3 | |||
| 3763577451 | |||
| c6618049f2 | |||
| 28d6aa9885 | |||
| 568786e4f6 | |||
| 2aad2d29de | |||
| 400354ee9e | |||
| ea3ba78d1a | |||
| 38614ee526 | |||
| 15ad6bb5f8 | |||
| 15f431ada6 | |||
| 5163acc6ef | |||
| f9297d783a | |||
| c34a02c06f | |||
| 61ed1fb202 | |||
| 87638ba6c5 | |||
| 26b4100bb0 | |||
| 1157c2adf6 | |||
| 8613d3288b | |||
| ab008fbdff | |||
| 0b30197280 | |||
| 89a121f21e | |||
| 045a8c56af | |||
| d83c11db8c | |||
| 7efe14b835 | |||
| 37116aecf9 | |||
| 9a606e8f2a | |||
| b1e65ed500 | |||
| 26f3dbff06 | |||
| 5ee0b188bd | |||
| 4692cd6a55 | |||
| 7ad8c01853 | |||
| bc27b45fd7 | |||
| 94cc28d377 | |||
| 19e80cef3b | |||
| 5397cd3194 | |||
| f01af9c0ec | |||
| a288010d02 | |||
| b9a7a1c32b | |||
| 65b9a0dce5 | |||
| 7464006e19 | |||
| 590d56b7e7 | |||
| c75cb1e87e | |||
| 597b2f4a6f | |||
| 2e84c6bbb2 | |||
| 06c0cd5297 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/resend": patch
|
||||
---
|
||||
|
||||
Sending emails should retry if there are errors
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/openai": patch
|
||||
---
|
||||
|
||||
Improved OpenAI task errors
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Changed the text on the ID question
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Don't return the apiKey when they don't match
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
replace tsconfig package with tsconfck package
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
getAllResponses fixed
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/resend": patch
|
||||
"@trigger.dev/plain": patch
|
||||
---
|
||||
|
||||
Missing token/apikey error improved for display in Next.js logs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
added defineJob in TriggerClient
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added support for Runs being canceled
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Set duplex "half" when creating fetch based Request objects when they have a body
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/plain": patch
|
||||
---
|
||||
|
||||
Added upsertCustomTimelineEntry Task and exported the components from the plain package
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/slack": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Upgrade to zod 3.21.4
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Parse .env file using dotenv instead of hand-parsing it
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added getEvent(), getRun() and getRuns() methods to the client
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/react": patch
|
||||
---
|
||||
|
||||
React hooks to get the live status of a run (and a run from a sent event)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Make isRetry context property backwards compatible and add it to the TriggerContext type
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/resend": patch
|
||||
"@trigger.dev/plain": patch
|
||||
---
|
||||
|
||||
Throw if the clients are passed an undefined apikey or token
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
Use a standalone tsconfig.json file to increase portability
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/github": patch
|
||||
---
|
||||
|
||||
Added getIssue Task
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Generate the README for a new integration package
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
This is a patch bump
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Adding the create-integration command to scaffold out new integration packages (inside or outside of the monorepo)
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
"@trigger.dev/integration-kit": patch
|
||||
"@trigger.dev/typeform": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/resend": patch
|
||||
"@trigger.dev/plain": patch
|
||||
"@trigger.dev/slack": patch
|
||||
"@trigger.dev/nextjs": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Set Node version to 16.8 and above
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Loosen the Next.js dependency version
|
||||
+49
-1
@@ -16,26 +16,46 @@
|
||||
"@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/typeform": "1.0.0-next.2"
|
||||
"@trigger.dev/typeform": "1.0.0-next.2",
|
||||
"@trigger.dev/plain": "1.0.0-next.2",
|
||||
"@trigger.dev/supabase": "0.1.0",
|
||||
"@trigger.dev/react": "1.0.0-next.1",
|
||||
"@trigger.dev/stripe": "0.0.1"
|
||||
},
|
||||
"changesets": [
|
||||
"afraid-bikes-draw",
|
||||
"afraid-hornets-cheer",
|
||||
"beige-maps-hug",
|
||||
"big-tables-search",
|
||||
"bright-buses-join",
|
||||
"bright-horses-bathe",
|
||||
"brown-bikes-bathe",
|
||||
"calm-scissors-smell",
|
||||
"chatty-cars-agree",
|
||||
"chilly-meals-warn",
|
||||
"clever-bats-learn",
|
||||
"cuddly-pets-arrive",
|
||||
"curly-hornets-sort",
|
||||
"dirty-needles-kneel",
|
||||
"dry-poems-joke",
|
||||
"dull-bulldogs-design",
|
||||
"eight-buckets-agree",
|
||||
"eighty-queens-prove",
|
||||
"empty-falcons-run",
|
||||
"fast-ligers-wink",
|
||||
"few-turtles-brush",
|
||||
"fifty-melons-enjoy",
|
||||
"forty-pugs-share",
|
||||
"four-ligers-perform",
|
||||
"friendly-parents-clean",
|
||||
"funny-elephants-sing",
|
||||
"gentle-dogs-end",
|
||||
"gentle-guests-leave",
|
||||
"giant-scissors-hug",
|
||||
"great-meals-listen",
|
||||
"green-flowers-wonder",
|
||||
"happy-bulldogs-jog",
|
||||
"healthy-houses-build",
|
||||
"healthy-rockets-cry",
|
||||
"heavy-eyes-own",
|
||||
"heavy-mails-explain",
|
||||
@@ -44,38 +64,66 @@
|
||||
"itchy-cars-count",
|
||||
"itchy-rockets-provide",
|
||||
"khaki-needles-wait",
|
||||
"lemon-ads-wait",
|
||||
"long-clocks-hide",
|
||||
"lovely-chicken-serve",
|
||||
"lovely-hounds-accept",
|
||||
"lovely-plants-brake",
|
||||
"lucky-crews-matter",
|
||||
"mean-sloths-destroy",
|
||||
"metal-donkeys-relate",
|
||||
"metal-trainers-work",
|
||||
"mighty-gorillas-march",
|
||||
"mighty-seals-fold",
|
||||
"modern-emus-suffer",
|
||||
"modern-turtles-eat",
|
||||
"moody-islands-melt",
|
||||
"odd-colts-smash",
|
||||
"pink-islands-pay",
|
||||
"pink-numbers-jam",
|
||||
"plenty-ties-raise",
|
||||
"popular-cooks-boil",
|
||||
"pretty-bats-obey",
|
||||
"pretty-zoos-approve",
|
||||
"quick-gifts-leave",
|
||||
"quick-stingrays-own",
|
||||
"quiet-berries-tickle",
|
||||
"quiet-dryers-collect",
|
||||
"rich-clocks-attack",
|
||||
"rich-suns-leave",
|
||||
"rude-bees-taste",
|
||||
"shiny-mirrors-beam",
|
||||
"shy-bobcats-hear",
|
||||
"silent-mirrors-lay",
|
||||
"silver-clocks-visit",
|
||||
"silver-crews-dance",
|
||||
"silver-insects-sneeze",
|
||||
"six-humans-happen",
|
||||
"sixty-pandas-deliver",
|
||||
"sixty-squids-wink",
|
||||
"spotty-dolphins-joke",
|
||||
"strange-jobs-shout",
|
||||
"strong-cars-visit",
|
||||
"stupid-files-cough",
|
||||
"stupid-islands-collect",
|
||||
"stupid-queens-relax",
|
||||
"sweet-adults-applaud",
|
||||
"sweet-cups-rescue",
|
||||
"sweet-houses-drum",
|
||||
"sweet-snails-refuse",
|
||||
"tame-flowers-invite",
|
||||
"tasty-bats-flow",
|
||||
"tasty-dragons-relate",
|
||||
"ten-birds-confess",
|
||||
"ten-llamas-develop",
|
||||
"thirty-apes-happen",
|
||||
"tidy-planes-compete",
|
||||
"tough-walls-share",
|
||||
"two-kangaroos-repeat",
|
||||
"two-tools-hide",
|
||||
"wild-rats-mix",
|
||||
"wise-pumas-destroy",
|
||||
"wise-shirts-visit",
|
||||
"young-birds-unite"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
safeBodyParse webhook import fixed
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
added isRetry in context run
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
All Typeform events should be "form_response". Got rid of "all"
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Add option to specify the path to the handler function in the dev command
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Fix when setting the url as an option
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
Update how tasks are exported
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Add cli init support for nextjs projects using javascript
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/openai": patch
|
||||
---
|
||||
|
||||
Adding support for output properties on tasks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
allow trigger CLI init command to use endpointId if it is present in package.json
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
Create a new webhook if the existing webhook url doesn't match
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/stripe": minor
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Initial Stripe integration
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
"@trigger.dev/integration-kit": patch
|
||||
---
|
||||
|
||||
integration-kit uses webhook package
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
All logs are now structured logs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Allow the CLI dev command to work outside of a Next.js project
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Adding some type helpers for getting the payload and IO types from jobs and triggers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Improved the endpoint prompt
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/react": patch
|
||||
---
|
||||
|
||||
Build fix for "use client" errors
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Improved spinner and made port clear in logs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Show the params to updateSource in the dashboard
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/supabase": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added io.integration.runTask and initial @trigger.dev/supabase integration
|
||||
+9
-1
@@ -3,6 +3,7 @@
|
||||
.github
|
||||
# editor
|
||||
.idea
|
||||
.vscode
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
@@ -34,4 +35,11 @@ cypress/screenshots
|
||||
cypress/videos
|
||||
|
||||
apps/**/styles/tailwind.css
|
||||
packages/**/styles/tailwind.css
|
||||
packages/**/styles/tailwind.css
|
||||
|
||||
.changeset
|
||||
examples
|
||||
CHANGESETS.md
|
||||
CONTRIBUTING.md
|
||||
README.md
|
||||
LICENSE
|
||||
|
||||
+10
-14
@@ -1,10 +1,3 @@
|
||||
# YOU MUST SET THESE VARIABLES
|
||||
# Resend is an email service used for signing in to Trigger.dev via a Magic Link.
|
||||
### Visit https://resend.com, create an account and get your API key. Then insert it below along with your From and Reply To email addresses. Visit https://resend.com/docs for more information.
|
||||
RESEND_API_KEY=<api_key>
|
||||
FROM_EMAIL=
|
||||
REPLY_TO_EMAIL=
|
||||
|
||||
# YOU MIGHT LIKE TO MODIFY THESE VARIABLES
|
||||
SESSION_SECRET=abcdef1234
|
||||
MAGIC_LINK_SECRET=abcdef1234
|
||||
@@ -16,17 +9,20 @@ APP_ORIGIN=http://localhost:3030
|
||||
NODE_ENV=development
|
||||
|
||||
# OPTIONAL VARIABLES
|
||||
AUTH_GITHUB_CLIENT_ID=
|
||||
AUTH_GITHUB_CLIENT_SECRET=
|
||||
# This is used for logging in via GitHub. You can leave these commented out if you don't want to use GitHub for authentication.
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
|
||||
# Resend is an email service used for signing in to Trigger.dev via a Magic Link.
|
||||
# Emails will print to the console if you leave these commented out
|
||||
### Visit https://resend.com, create an account and get your API key. Then insert it below along with your From and Reply To email addresses. Visit https://resend.com/docs for more information.
|
||||
# RESEND_API_KEY=<api_key>
|
||||
# FROM_EMAIL=
|
||||
# REPLY_TO_EMAIL=
|
||||
|
||||
# CLOUD VARIABLES
|
||||
POSTHOG_PROJECT_KEY=
|
||||
PLAIN_API_KEY=
|
||||
SEED_CLOUD=
|
||||
SEED_CLOUD_EMAIL=
|
||||
SEED_CLOUD_GITHUB_ACCESS_TOKEN=
|
||||
SEED_CLOUD_SLACK_ACCESS_TOKEN=
|
||||
SEED_CLOUD_USER_GITHUB_ACCESS_TOKEN=
|
||||
CLOUD_AIRTABLE_CLIENT_ID=
|
||||
CLOUD_AIRTABLE_CLIENT_SECRET=
|
||||
CLOUD_GITHUB_CLIENT_ID=
|
||||
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
# Contributing to Trigger.dev
|
||||
|
||||
Thank you for taking the time to contribute to Trigger.dev. Your involvement is not just welcomed, but we encourage it! 🚀
|
||||
|
||||
Please take some time to read this guide to understand contributing best practices for Trigger.dev.
|
||||
|
||||
Thank you for helping us make Trigger.dev even better! 🤩
|
||||
|
||||
## Developing
|
||||
|
||||
The development branch is `main`. This is the branch that all pull
|
||||
requests should be made against. The changes on the `main`
|
||||
branch are tagged into a release monthly.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/en) version >=16.x
|
||||
- [pnpm package manager](https://pnpm.io/installation) version 7
|
||||
- [Docker](https://www.docker.com/get-started/)
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repo into a public GitHub repository or [fork the repo](https://github.com/triggerdotdev/trigger.dev/fork). If you plan to distribute the code, keep the source code public to comply with the [Apache Licence 2.0](https://github.com/triggerdotdev/trigger.dev/blob/main/LICENSE).
|
||||
|
||||
```
|
||||
git clone https://github.com/triggerdotdev/trigger.dev.git
|
||||
```
|
||||
|
||||
> If you are on windows, run the following command on gitbash with admin privileges:
|
||||
> `git clone -c core.symlinks=true https://triggerdotdev/trigger.dev.git`
|
||||
|
||||
2. Navigate to the project folder
|
||||
```
|
||||
cd trigger.dev
|
||||
```
|
||||
3. Install the required packages using pnpm.
|
||||
```
|
||||
pnpm i
|
||||
```
|
||||
4. Create your `.env` files
|
||||
```
|
||||
cp .env.example .env && cp packages/database/.env.example packages/database/.env
|
||||
```
|
||||
5. Open the root `.env` file and fill in the required values Magic Link:
|
||||
|
||||
Both of these secrets should be random strings, which you can easily generate (and copy into your pasteboard) with the following command:
|
||||
|
||||
```sh
|
||||
openssl rand -hex 16 | pbcopy
|
||||
```
|
||||
|
||||
<p>Then set them here:</p>
|
||||
|
||||
```
|
||||
SESSION_SECRET=<string>
|
||||
MAGIC_LINK_SECRET=<string>
|
||||
```
|
||||
|
||||
6. Start Docker. This starts the required services like Postgres. If this is your first time using Docker, consider going through this [guide](DOCKER_INSTALLATION.md)
|
||||
```
|
||||
pnpm run docker
|
||||
```
|
||||
7. Migrate the database
|
||||
```
|
||||
pnpm run db:migrate
|
||||
```
|
||||
8. Build the app
|
||||
```
|
||||
pnpm run build --filter webapp
|
||||
```
|
||||
9. Run the seed script
|
||||
```
|
||||
pnpm run db:seed
|
||||
```
|
||||
10. Run the app. See the section below.
|
||||
|
||||
## Running
|
||||
1. You can run the app with:
|
||||
|
||||
```
|
||||
pnpm run dev --filter webapp
|
||||
```
|
||||
|
||||
It should run on port `3030`: [http://localhost:3030](http://localhost:3030/)
|
||||
|
||||
2. Once the app is running click the magic link button and enter your email.
|
||||
3. Check your terminal, the magic link email should have printed out as following:
|
||||
``
|
||||
webapp:dev: Log in to Trigger.dev
|
||||
webapp:dev:
|
||||
webapp:dev: Click here to log in with this magic link
|
||||
webapp:dev: [http://localhost:3030/magic?token=U2FsdGVkX18OvB0JxgaswTLCSbaRz%2FY82TN0EZWhSzFyZYwgG%2BIzKVTkeiaOtWfotPw7F8RwFzCHh53aBpMEu%2B%2B%2FItb%2FcJYh89MSjc3Pz92bevoEjqxSQ%2Ff%2BZbks09JOpqlBbYC3FzGWC8vuSVFBlxqLXxteSDLthZSUaC%2BS2LaA%2BJgp%2BLO7hgjAaC2lXbCHrM7MTgTdXOFt7i0Dvvuwz6%2BWY25RnfomZOPqDsyH0xz8Q2rzPTz0Xu53WSXrZ1hd]
|
||||
webapp:dev:
|
||||
webapp:dev: If you didn't try to log in, you can safely ignore this email.
|
||||
``
|
||||
4. Paste the magic link shown in your terminal into your browser to login.
|
||||
|
||||
## Testing CLI changes
|
||||
To test CLI changes, follow the steps below:
|
||||
|
||||
1. Build the CLI and watch for changes
|
||||
|
||||
```
|
||||
cd packages/cli
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
2. Open a new Terminal window and run the webapp locally and then create a new project in the dashboard. Copy out the dev API key.
|
||||
|
||||
3. Create a new temporary Next.js app in examples directory
|
||||
|
||||
```
|
||||
pnpm create next-app@latest
|
||||
```
|
||||
|
||||
Follow the prompts to create a TypeScript project using the App Directory.
|
||||
|
||||
4. Then once that's finished, add the `@trigger.dev/cli` to the `devDependencies` of the newly created Next.js app's `package.json` file, like so:
|
||||
|
||||
```
|
||||
{
|
||||
"devDependencies": { "@trigger.dev/cli": "workspace:*" }
|
||||
}
|
||||
```
|
||||
|
||||
5. Open a new terminal window, navigate into the example, and initialize the CLI:
|
||||
|
||||
```
|
||||
cd examples/your-newly-created-nextjs-project
|
||||
pnpm i
|
||||
pnpm exec trigger-cli init
|
||||
```
|
||||
|
||||
6. When prompted, select `self-hosted` and enter `localhost:3030` for your local version of the webapp. When asked for an API key, use the key you copied earlier.
|
||||
|
||||
7. Run the CLI
|
||||
```
|
||||
pnpm exec trigger-cli dev
|
||||
```
|
||||
|
||||
8. After running the CLI, start your newly created Next.js project. You should now be able to see the changes.
|
||||
|
||||
9. Please remember to delete the temporary project you created after you've tested the changes, and before you raise a PR.
|
||||
## Add sample jobs
|
||||
|
||||
The [examples/jobs-starter](./examples/jobs-starter/) project defines simple jobs you can get started with.
|
||||
|
||||
1. `cd` into `examples/jobs-starter`
|
||||
2. Create a `.env.local` file with the following content,
|
||||
replacing `[TRIGGER_DEV_API_KEY]` with an actual key:
|
||||
|
||||
```
|
||||
TRIGGER_API_KEY=[TRIGGER_DEV_API_KEY]
|
||||
TRIGGER_API_URL=http://localhost:3030
|
||||
```
|
||||
|
||||
`TRIGGER_API_URL` is used to configure the URL for your Trigger.dev instance,
|
||||
where the jobs will be registered.
|
||||
|
||||
3. Run the `jobs-starter` app:
|
||||
|
||||
```
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. Navigate to your trigger.dev instance ([http://localhost:3030](http://localhost:3030/)), to see the jobs.
|
||||
You can use the test feature to trigger them.
|
||||
|
||||
## Making a pull request
|
||||
|
||||
**If you get errors, be sure to fix them before committing.**
|
||||
|
||||
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating you PR.
|
||||
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue
|
||||
](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
||||
- Be sure to fill the PR Template accordingly.
|
||||
|
||||
## Adding changesets
|
||||
|
||||
We use [changesets](https://github.com/changesets/changesets) to manage our package versions and changelogs. If you've never used changesets before, first read [their guide here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
|
||||
|
||||
If you are contributing a change to any packages in this monorepo (anything in either the `/packages` or `/integrations` directories), then you will need to add a changeset to your Pull Requests before they can be merged.
|
||||
|
||||
To add a changeset, run the following command in the root of the repo
|
||||
|
||||
```shell
|
||||
pnpm run changeset:add
|
||||
```
|
||||
|
||||
Here's an example of creating a `patch` changeset for the `@trigger.dev/github` and `@trigger.dev/slack` packages (click to view):
|
||||
|
||||
[](https://asciinema.org/a/599228)
|
||||
|
||||
You will be prompted to select which packages to include in the changeset. Only select the packages that you have made changes for (Important: never include a changeset for the `@trigger.dev/internal` package, as it's bundled with other packages and should not be published).
|
||||
|
||||
Most of the time the changes you'll make are likely to be categorized as patch releases. If you feel like there is the need for a minor or major release of the package based on the changes being made, add the changeset as such and it will be discussed during PR review.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### EADDRINUSE: address already in use :::3030
|
||||
|
||||
When receiving the following error message:
|
||||
``
|
||||
webapp:dev: Error: listen EADDRINUSE: address already in use :::3030
|
||||
``
|
||||
|
||||
The process running on port `3030` should be destroyed.
|
||||
|
||||
1. Get the `PID` of the process running on PORT `3030`
|
||||
```
|
||||
lsof -i :3030
|
||||
```
|
||||
2. Kill the process
|
||||
```
|
||||
sudo kill -9 <PID>
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
# Trigger.dev Deployment Guide
|
||||
|
||||
## StreamNative Cloud - Hosted Pulsar
|
||||
|
||||
### Generic client credentials
|
||||
|
||||
```sh
|
||||
snctl auth export-service-account webapp --key-file webapp-credentials.json
|
||||
snctl auth export-service-account websocketserver --key-file wss-credentials.json
|
||||
```
|
||||
|
||||
### Topics
|
||||
|
||||
#### Triggers (`persistent://triggerdotdev/workflows/triggers`)
|
||||
|
||||
Events that trigger workflows to run. These are sent by the "platform" and read by the Web Socket Servers, which then coordinate with the hosts for running the workflows
|
||||
|
||||
#### Run Commands (`persistent://triggerdotdev/workflows/run-commands`)
|
||||
|
||||
These are events that come from hosts and are published by the Web Socket Servers, e.g. Sending Integration Requests, Sending Logs, Initializing a Delay
|
||||
|
||||
#### Run Command Responses (`persistent://triggerdotdev/workflows/run-command-responses`)
|
||||
|
||||
These are events that come from the platform and are read by the Web Socket Servers, to resolve or reject a previous Run Command
|
||||
|
||||
#### Integration Requests (`persistent://triggerdotdev/queues/integration-requests`)
|
||||
|
||||
This is an internal queue used by the platform to perform integration requests, and retry them.
|
||||
|
||||
#### App Task Queue (`persistent://triggerdotdev/queues/background-tasks`)
|
||||
|
||||
This is an internal queue used by the platform to do tasks in a queue. Basically a background job system.
|
||||
-231
@@ -1,231 +0,0 @@
|
||||
# Initial setup
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Pulsar requirements
|
||||
|
||||
1. Ensure you have Homebrew installed by running `which brew` in terminal. If it's not found then you should install it: https://brew.sh/. Run `which brew` again to check it's found. If it's not you may need to [add it your path](https://stackoverflow.com/questions/36657321/after-installing-homebrew-i-get-zsh-command-not-found-brew)
|
||||
|
||||
2. Run `brew install libpulsar` to install the C++ libraries that the pulsar-client depends on
|
||||
|
||||
3. Make sure you have Python installed on your machine by running `which python3` in terminal.
|
||||
|
||||
4. If python isn't found then you should install it: https://www.python.org/downloads/. In a new terminal window run `which python3` again.
|
||||
|
||||
5. Run `npm config set python /the/path/from/the/which/python3/command` inserting the path from step 2 or 3
|
||||
|
||||
6. Install node-gyp: `npm install -g node-gyp`
|
||||
|
||||
7. Make sure you have the Xcode command line tools installed by running `xcode-select --install` from the terminal. If it says they're already installed then you're set.
|
||||
|
||||
8. Run this in the terminal:
|
||||
|
||||
```sh
|
||||
export CPLUS_INCLUDE_PATH="$CPLUS_INCLUDE_PATH:$(brew --prefix)/include"
|
||||
export LIBRARY_PATH="$LIBRARY_PATH:$(brew --prefix)/lib"
|
||||
export PULSAR_CPP_DIR=/opt/homebrew/Cellar/libpulsar/3.1.0
|
||||
```
|
||||
|
||||
9. Run `pnpm install` in the same terminal window.
|
||||
|
||||
## Building and running the webapp
|
||||
|
||||
> **Warning**
|
||||
> All the following commands should be launched from the **monorepo root directory**
|
||||
|
||||
1. Install the dependencies.
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
2. Optionally, if you are testing auth (pizzly) or webhooks (webapp) then you'll need to use ngrok to proxy internet traffic to your local machine
|
||||
|
||||
Get access to ngrok and then follow the instructions here to get it setup: https://ngrok.com/download (use homebrew) and make sure to authenticate.
|
||||
|
||||
Then run the following scripts to start proxying:
|
||||
|
||||
```sh
|
||||
./scripts/proxy-pizzly.sh dan-pizzly-dev
|
||||
./scripts/proxy-webapp.sh dan-trigger-dev
|
||||
```
|
||||
|
||||
3. Environment variables. You will need to create copies of the `.env.example` files in `app/webapp`
|
||||
|
||||
```sh
|
||||
cp ./apps/webapp/.env.example ./apps/webapp/.env
|
||||
```
|
||||
|
||||
Then you will need to fill in the fields with real values.
|
||||
|
||||
You also need to create the `pizzly-server.env` files under the `.docker` directory:
|
||||
|
||||
```sh
|
||||
cp ./.docker/pizzly-server.env.example ./.docker/pizzly-server.env
|
||||
```
|
||||
|
||||
Next, update the `AUTH_CALLBACK_URL` env var in the `pizzly-server.env` env file with the value provided to the `./scripts/proxy-pizzly.sh` command. Using the example above the `AUTH_CALLBACK_URL` would be `AUTH_CALLBACK_URL=https://dan-pizzly-dev.eu.ngrok.io/oauth/callback`.
|
||||
|
||||
If you aren't proxying pizzly according to step 2, then leave the `pizzly-server.env` file empty.
|
||||
|
||||
If you are proxying the webapp according to step 2 then in `webapp/.env`, set the `APP_ORIGIN` to the `NGROK_SUBDOMAIN` provided to the `./scripts/proxy-webapp.sh` command, e.g. `APP_ORIGIN=https://dan-trigger-dev.eu.ngrok.io`
|
||||
|
||||
4. Start postgresql, redis, pizzly server, followed by pulsar standalone
|
||||
|
||||
```bash
|
||||
pnpm run docker:services
|
||||
pnpm run docker:pulsar
|
||||
```
|
||||
|
||||
> **Note:** The npm script will complete while Docker sets up the container in the background. Ensure that Docker has finished and your container is running before proceeding.
|
||||
|
||||
5. Generate prisma schema
|
||||
```bash
|
||||
pnpm run generate
|
||||
```
|
||||
6. Run the Prisma migration to the database
|
||||
|
||||
```bash
|
||||
pnpm run db:migrate:deploy
|
||||
```
|
||||
|
||||
7. Run the first build (with dependencies via the `...` option)
|
||||
|
||||
```bash
|
||||
pnpm run build --filter=webapp...
|
||||
```
|
||||
|
||||
**Running simply `pnpm run build` will build everything, including the Remix app.**
|
||||
|
||||
8. Run the Remix dev server
|
||||
|
||||
```bash
|
||||
pnpm run dev --filter=webapp
|
||||
```
|
||||
|
||||
## Attaching an API integration using Pizzly
|
||||
|
||||
```bash
|
||||
PIZZLY_HOSTPORT=http://localhost:3004 npx pizzly config:create github github <client-id> <client-secret> "repo,user"
|
||||
```
|
||||
|
||||
## Tests, Typechecks, Lint, Install packages...
|
||||
|
||||
Check the `turbo.json` file to see the available pipelines.
|
||||
|
||||
- Run the Cypress tests and Dev
|
||||
```bash
|
||||
pnpm run test:e2e:dev --filter=webapp
|
||||
```
|
||||
- Lint everything
|
||||
```bash
|
||||
pnpm run lint
|
||||
```
|
||||
- Typecheck the whole monorepo
|
||||
```bash
|
||||
pnpm run typecheck
|
||||
```
|
||||
- Test the whole monorepo
|
||||
```bash
|
||||
pnpm run test
|
||||
or
|
||||
pnpm run test:dev
|
||||
```
|
||||
- How to install an npm package in the Remix app ?
|
||||
```bash
|
||||
pnpm add dayjs --filter webapp
|
||||
```
|
||||
- Tweak the tsconfigs, eslint configs in the `config-package` folder. Any package or app will then extend from these configs.
|
||||
|
||||
# Running a workflow locally
|
||||
|
||||
## After pulling a change
|
||||
|
||||
1. Ensure there are no database migrations to run
|
||||
|
||||
```bash
|
||||
pnpm run db:migrate:dev
|
||||
```
|
||||
|
||||
2. Generate the Prisma database client
|
||||
|
||||
```bash
|
||||
pnpm run generate
|
||||
```
|
||||
|
||||
3. Install packages
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
4. Build everything
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
5. Install packages again, this makes sure the local packages are linked
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Running the servers
|
||||
|
||||
1. Ensure the docker containers are running
|
||||
|
||||
```bash
|
||||
pnpm run docker:services
|
||||
pnpm run docker:pulsar
|
||||
```
|
||||
|
||||
2. Run the webapp
|
||||
|
||||
```bash
|
||||
pnpm run dev --filter=webapp
|
||||
```
|
||||
|
||||
3. Run the Web Socket Server
|
||||
|
||||
```bash
|
||||
pnpm run dev --filter=wss
|
||||
```
|
||||
|
||||
4. Build all the @trigger.dev/\* packages
|
||||
|
||||
```bash
|
||||
pnpm run dev --filter="@trigger.dev/*"
|
||||
```
|
||||
|
||||
## Running the smoke test
|
||||
|
||||
1. Run the smoke test workflow
|
||||
|
||||
```bash
|
||||
cd ./examples/smoke-test
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
2. Running the workflow requires you to send data to the local API.
|
||||
|
||||
You can use this cURL command to send a `user.created` event. This will run the workflow and generate the corresponding logs.
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://localhost:3000/api/v1/events \
|
||||
--header 'Authorization: Bearer trigger_dev_zC25mKNn6c0q' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "user.created",
|
||||
"payload": {
|
||||
"id": "123"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Dependency & Package graph
|
||||
|
||||

|
||||
|
||||
## Warp Launch Configuration
|
||||
|
||||
Setup a custom launch configuration for the Warp terminal ([docs here](https://docs.warp.dev/features/sessions/launch-configurations)) by copying the `.warp/triggerdotdev.yaml.example` file to `~/.warp/launch_configurations/triggerdotdev.yaml`. Make sure you edit the file and replace `<your-trigger-dev>` and `<your-pizzly-dev>` with your custom ngrok subdomains.
|
||||
@@ -0,0 +1,126 @@
|
||||
This guide covers installing Docker and Docker Compose. If you're looking for instructions for running Trigger.dev in docker, [see here](https://github.com/triggerdotdev/docker).
|
||||
|
||||
## Setting up Docker for the first time.
|
||||
|
||||
In the contributing guide of Trigger.dev, there's a section that requires you to start Docker.
|
||||
|
||||
If you don't have Docker installed on your machine, you'll run into some complications (errors).
|
||||
|
||||
Below are the steps on how you can avoid that.
|
||||
|
||||
First you need to setup docker-compose as it is an underlying tool that this command: `pnpm run docker` fires behind the scene.
|
||||
|
||||
## Linux
|
||||
|
||||
To install Docker Compose on Linux Ubuntu via the terminal, you can follow these steps:
|
||||
|
||||
1. Update the package index on your system by running the following command:
|
||||
|
||||
```shell
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
2. Install the required dependencies by running the following command:
|
||||
|
||||
```shell
|
||||
sudo apt install curl
|
||||
```
|
||||
|
||||
3. Download the Docker Compose binary into the `/usr/local/bin` directory using the `curl` command:
|
||||
|
||||
```shell
|
||||
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
4. Set the appropriate permissions to make the `docker-compose` binary executable:
|
||||
|
||||
```shell
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
```
|
||||
|
||||
5. Verify that Docker Compose has been successfully installed by running the following command:
|
||||
|
||||
```shell
|
||||
docker-compose --version
|
||||
```
|
||||
|
||||
This command should display the version information of Docker Compose without any errors.
|
||||
|
||||
After following these steps, you should have Docker Compose installed on your Ubuntu system, and you can use it by running `docker-compose` commands in the terminal.
|
||||
|
||||
When you've verified that the `docker-compose` package is installed and you proceed to start Docker with `pnpm run docker`.
|
||||
|
||||
You'll probably get an error similar to the one below:
|
||||
|
||||
```shell
|
||||
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
|
||||
ELIFECYCLE Command failed with exit code 1.
|
||||
```
|
||||
|
||||
The error message suggests that the Docker daemon is not running on your system. The Docker daemon is responsible for managing and running Docker containers.
|
||||
|
||||
To resolve this issue, you may need to install Docker properly on your Ubuntu system. Here are the steps to install Docker on Ubuntu:
|
||||
|
||||
1. Update the package index on your system by running the following command:
|
||||
|
||||
```shell
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
2. Install the necessary packages to allow apt to use repositories over HTTPS:
|
||||
|
||||
```shell
|
||||
sudo apt install apt-transport-https ca-certificates curl software-properties-common
|
||||
```
|
||||
|
||||
3. Add the official Docker GPG key to your system by running the following command:
|
||||
|
||||
```shell
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
```
|
||||
|
||||
4. Add the Docker repository to the APT sources list:
|
||||
|
||||
```shell
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
```
|
||||
|
||||
5. Update the package index again:
|
||||
|
||||
```shell
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
6. Install Docker by running the following command:
|
||||
|
||||
```shell
|
||||
sudo apt install docker-ce docker-ce-cli containerd.io
|
||||
```
|
||||
|
||||
7. After the installation is complete, verify that Docker is installed correctly by running the following command:
|
||||
|
||||
```shell
|
||||
docker --version
|
||||
```
|
||||
|
||||
This command should display the version information of Docker without any errors.
|
||||
|
||||
Once Docker is installed and verified, you should be able to start the Docker daemon and run the `pnpm run docker` command without encountering any issues.
|
||||
|
||||
## Windows
|
||||
|
||||
1. Download the Docker Desktop installer from the Docker website: [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop)
|
||||
|
||||
2. Run the installer and follow the instructions to install Docker Desktop.
|
||||
|
||||
3. After installation, Docker Desktop should be running automatically.
|
||||
|
||||
## macOS
|
||||
|
||||
1. Download the Docker Desktop installer from the Docker website: [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop)
|
||||
|
||||
2. Run the installer and follow the instructions to install Docker Desktop.
|
||||
|
||||
3. After installation, Docker Desktop should be running automatically.
|
||||
|
||||
Please note that the instructions provided above are for the most common scenarios. For specific versions or different distributions, it's always a good idea to consult the official Docker documentation for the respective operating systems.
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
### The background jobs framework for Next.js
|
||||
|
||||
[Discord](https://discord.gg/bpVDSjcG) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
|
||||
[Discord](https://discord.gg/JtBAxBr2m3) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
|
||||
|
||||
[](https://twitter.com/triggerdotdev)
|
||||
[](https://github.com/triggerdotdev/trigger.dev)
|
||||
@@ -60,46 +60,5 @@ We provide an official trigger.dev docker image you can use to easily self-host
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
- [Node.js](https://nodejs.org/en) version >=18.x
|
||||
- [pnpm package manager](https://pnpm.io/installation) version 7
|
||||
- [Docker](https://www.docker.com/get-started/)
|
||||
To setup and develop locally or contribute to the open source project, follow our [developement guide](./CONTRIBUTING.md).
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repo into a public GitHub repository or [fork the repo](https://github.com/triggerdotdev/trigger.dev/fork). If you plan to distribute the code, keep the source code public to comply with the [Apache Licence 2.0](https://github.com/triggerdotdev/trigger.dev/blob/main/LICENSE).
|
||||
|
||||
```
|
||||
git clone https://github.com/triggerdotdev/trigger.dev.git
|
||||
```
|
||||
> If you are on windows, run the following command on gitbash with admin privileges:
|
||||
> `git clone -c core.symlinks=true https://triggerdotdev/trigger.dev.git`
|
||||
2. Navigate to the project folder
|
||||
```
|
||||
cd trigger.dev
|
||||
```
|
||||
3. Install the required packages using pnpm.
|
||||
```
|
||||
pnpm i
|
||||
```
|
||||
4. Create your `.env` files
|
||||
```
|
||||
cp .env.example .env && cp packages/database/.env.example packages/database/.env
|
||||
```
|
||||
Open the root `.env` file and fill in the required values.
|
||||
6. Start Docker. This starts the required services like Postgres.
|
||||
```
|
||||
pnpm run docker
|
||||
```
|
||||
7. Migrate the database
|
||||
```
|
||||
pnpm run db:migrate
|
||||
```
|
||||
8. Run the seed script
|
||||
```
|
||||
pnpm run db:seed
|
||||
```
|
||||
5. Run the project. It should run on `port:3030`
|
||||
```
|
||||
pnpm run dev --filter webapp
|
||||
```
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export function ErrorIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="10" cy="10" r="7" fill="white" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10ZM10 5C10.4142 5 10.75 5.33579 10.75 5.75V10.25C10.75 10.6642 10.4142 11 10 11C9.58579 11 9.25 10.6642 9.25 10.25V5.75C9.25 5.33579 9.58579 5 10 5ZM10 15C10.5523 15 11 14.5523 11 14C11 13.4477 10.5523 13 10 13C9.44771 13 9 13.4477 9 14C9 14.5523 9.44771 15 10 15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
Form,
|
||||
useActionData,
|
||||
useLocation,
|
||||
useNavigation,
|
||||
} from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { feedbackTypeLabel, schema } from "~/routes/resources.feedback";
|
||||
import { ReactNode, useState } from "react";
|
||||
import {
|
||||
FeedbackType,
|
||||
feedbackTypeLabel,
|
||||
schema,
|
||||
} from "~/routes/resources.feedback";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { Fieldset } from "./primitives/Fieldset";
|
||||
import { FormButtons } from "./primitives/FormButtons";
|
||||
@@ -33,7 +36,12 @@ import {
|
||||
} from "./primitives/Sheet";
|
||||
import { TextArea } from "./primitives/TextArea";
|
||||
|
||||
export function Feedback() {
|
||||
type FeedbackProps = {
|
||||
button: ReactNode;
|
||||
defaultValue?: FeedbackType;
|
||||
};
|
||||
|
||||
export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const lastSubmission = useActionData();
|
||||
@@ -58,20 +66,13 @@ export function Feedback() {
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild={true}>
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
LeadingIcon={ChatBubbleLeftRightIcon}
|
||||
shortcut={{ key: "f" }}
|
||||
>
|
||||
Send us feedback
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetTrigger asChild={true}>{button}</SheetTrigger>
|
||||
<SheetContent size="sm">
|
||||
<SheetHeader className="justify-between">Give us feedback</SheetHeader>
|
||||
<SheetHeader className="justify-between">Help & feedback</SheetHeader>
|
||||
<SheetBody>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
We'd love to hear your feedback, good, bad or ugly.
|
||||
Use this form to ask for help or give us feedback. We read every
|
||||
message and will get back to you as soon as we can.
|
||||
</Paragraph>
|
||||
<Form method="post" action="/resources/feedback" {...form.props}>
|
||||
<Fieldset>
|
||||
@@ -80,9 +81,12 @@ export function Feedback() {
|
||||
{...conform.input(path, { type: "hidden" })}
|
||||
/>
|
||||
<InputGroup>
|
||||
<Label>What kind of feedback do you have?</Label>
|
||||
<Label>How can we help?</Label>
|
||||
<SelectGroup>
|
||||
<Select {...conform.input(feedbackType)} defaultValue={"bug"}>
|
||||
<Select
|
||||
{...conform.input(feedbackType)}
|
||||
defaultValue={defaultValue}
|
||||
>
|
||||
<SelectTrigger size="medium" width="full">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Paragraph, TextLink } from "~/components/primitives/Paragraph";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
@@ -8,7 +8,9 @@ import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
|
||||
import { jobTestPath } from "~/utils/pathBuilder";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { HelpPanelProps } from "../integrations/ApiKeyHelp";
|
||||
import { HelpInstall } from "../integrations/HelpInstall";
|
||||
import { HelpSamples } from "../integrations/HelpSamples";
|
||||
@@ -21,6 +23,8 @@ import {
|
||||
ClientTabsTrigger,
|
||||
} from "../primitives/ClientTabs";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
import integrationButton from "./integration-button.png";
|
||||
import selectEnvironment from "./select-environment.png";
|
||||
import selectExample from "./select-example.png";
|
||||
@@ -30,11 +34,11 @@ export function HowToSetupYourProject() {
|
||||
const appOrigin = useAppOrigin();
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Run the CLI init command" />
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Run the CLI 'init' command in a Next.js project"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Run this CLI command in a terminal window from your Next.js project.
|
||||
</Paragraph>
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
@@ -61,39 +65,57 @@ export function HowToSetupYourProject() {
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure={`yarn @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`yarn @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
|
||||
secure={`yarn dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`yarn dlx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
<Paragraph spacing variant="small">
|
||||
It will ask you for a "unique ID for your endpoint". You can use the
|
||||
default by hitting enter.
|
||||
|
||||
<Paragraph spacing>
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added
|
||||
a very simple example Job in <InlineCode>examples.ts</InlineCode> to
|
||||
help you get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Next.js app" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Ensure your Next.js app is running locally on port 3000.
|
||||
</Paragraph>
|
||||
<Paragraph>Ensure your app is running locally.</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={`npm run dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm run dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn run dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI dev command" />
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
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>{" "}
|
||||
<strong className="text-bright">
|
||||
separate terminal window or tab
|
||||
</strong>{" "}
|
||||
run:
|
||||
</Paragraph>
|
||||
<ClientTabs defaultValue="npm">
|
||||
@@ -120,10 +142,18 @@ export function HowToSetupYourProject() {
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn @trigger.dev/cli@latest dev`}
|
||||
value={`yarn dlx @trigger.dev/cli@latest dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
<Paragraph spacing variant="small">
|
||||
If you’re not running on port 3000 you can specify the port by adding{" "}
|
||||
<InlineCode>--port 3001</InlineCode> to the end.
|
||||
</Paragraph>
|
||||
<Paragraph spacing variant="small">
|
||||
You should leave the <InlineCode>dev</InlineCode> command running when
|
||||
you're developing.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="4" title="Check for Jobs" />
|
||||
<StepContentContainer>
|
||||
@@ -318,6 +348,101 @@ export function HowToUseThisIntegration({
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToUseApiKeysAndEndpoints() {
|
||||
return (
|
||||
<>
|
||||
<Paragraph spacing>
|
||||
Environments and Endpoints are used to connect your server to the
|
||||
Trigger.dev platform.
|
||||
</Paragraph>
|
||||
<Header2 spacing>Environments</Header2>
|
||||
<Paragraph spacing>
|
||||
Each environment has API Keys associated with it. The Server API Key is
|
||||
used to authenticate your Jobs with the Trigger.dev platform.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
The Server API Key you use for your{" "}
|
||||
<TextLink to="https://trigger.dev/docs/documentation/concepts/client-adaptors">
|
||||
Client
|
||||
</TextLink>{" "}
|
||||
is how we know which environment to run your code against:
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
showLineNumbers={false}
|
||||
className="mb-4"
|
||||
code={`export const client = new TriggerClient({
|
||||
id: "nextjs-example",
|
||||
//this environment variable should be set to your Server DEV API Key locally,
|
||||
//and your Server PROD API Key in production
|
||||
apiKey: process.env.TRIGGER_API_KEY!,
|
||||
});`}
|
||||
/>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<span>Development</span>
|
||||
<EnvironmentLabel environment={{ type: "DEVELOPMENT" }} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
The <InlineCode>DEV</InlineCode> environment should only be used for
|
||||
local development. It’s where you can test your Jobs before deploying
|
||||
them to servers.
|
||||
</Paragraph>
|
||||
<Callout variant="warning" className="my-2">
|
||||
Scheduled Triggers do not trigger Jobs in the DEV Environment. When
|
||||
you’re working locally you should use the Test feature to trigger any
|
||||
scheduled Jobs.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<span>Production</span>
|
||||
<EnvironmentLabel environment={{ type: "PRODUCTION" }} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
The <InlineCode>PROD</InlineCode> environment is where your Jobs will
|
||||
run in production. It’s where you can run your Jobs against real data.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<Header2 spacing>Endpoints</Header2>
|
||||
<Paragraph spacing>
|
||||
An Endpoint is a URL on your server that Trigger.dev can connect to.
|
||||
This URL is used to register Jobs, start them and orchestrate runs and
|
||||
retries.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
<InlineCode>DEV</InlineCode> has multiple endpoints associated with it –
|
||||
one for each team member. This allows each team member to run their own
|
||||
Jobs, without interfering with each other.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
All other environments have just a single endpoint (with a single URL)
|
||||
associated with them.
|
||||
</Paragraph>
|
||||
<Header2 spacing>Deployment</Header2>
|
||||
<Paragraph spacing>
|
||||
Deployment uses Environments and Endpoints to connect your Jobs to the
|
||||
Trigger.dev platform.
|
||||
</Paragraph>
|
||||
<Callout
|
||||
variant="docs"
|
||||
to="https://trigger.dev/docs/documentation/guides/deployment"
|
||||
>
|
||||
Read the deployment guide to learn more.
|
||||
</Callout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StepContentContainer({ children }: { children: React.ReactNode }) {
|
||||
return <div className="mb-6 ml-9 mt-1">{children}</div>;
|
||||
}
|
||||
|
||||
@@ -28,12 +28,14 @@ export function ConnectToIntegrationSheet({
|
||||
button,
|
||||
className,
|
||||
callbackUrl,
|
||||
icon,
|
||||
}: {
|
||||
integration: Integration;
|
||||
organizationId: string;
|
||||
button: React.ReactNode;
|
||||
callbackUrl: string;
|
||||
className?: string;
|
||||
icon?: string;
|
||||
}) {
|
||||
const [integrationMethod, setIntegrationMethod] = useState<
|
||||
IntegrationMethod | undefined
|
||||
@@ -48,7 +50,10 @@ export function ConnectToIntegrationSheet({
|
||||
<SheetTrigger className={className}>{button}</SheetTrigger>
|
||||
<SheetContent size="lg" className="relative">
|
||||
<SheetHeader>
|
||||
<NamedIconInBox name={integration.identifier} className="h-9 w-9" />
|
||||
<NamedIconInBox
|
||||
name={icon ?? integration.identifier}
|
||||
className="h-9 w-9"
|
||||
/>
|
||||
<div className="grow">
|
||||
<Header1>{integration.name}</Header1>
|
||||
{integration.description && (
|
||||
@@ -59,6 +64,7 @@ export function ConnectToIntegrationSheet({
|
||||
to={docsIntegrationPath(integration.identifier)}
|
||||
variant="secondary/small"
|
||||
LeadingIcon="docs"
|
||||
target="_blank"
|
||||
>
|
||||
View docs
|
||||
</LinkButton>
|
||||
|
||||
@@ -60,6 +60,7 @@ export function ConnectToOAuthForm({
|
||||
},
|
||||
] = useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
shouldRevalidate: "onSubmit",
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
// Create the schema without any constraint defined
|
||||
@@ -187,15 +188,16 @@ export function ConnectToOAuthForm({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Header2>Scopes</Header2>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Select the scopes you want to grant to {integration.name} in order
|
||||
for it to access your data. Note: If you try and perform an action
|
||||
in a Job that requires a scope you haven’t granted, that task will
|
||||
fail.
|
||||
</Paragraph>
|
||||
{/* <Header3 className="mb-2">
|
||||
{authMethod.scopes.length > 0 && (
|
||||
<div>
|
||||
<Header2>Scopes</Header2>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Select the scopes you want to grant to {integration.name} in order
|
||||
for it to access your data. Note: If you try and perform an action
|
||||
in a Job that requires a scope you haven’t granted, that task will
|
||||
fail.
|
||||
</Paragraph>
|
||||
{/* <Header3 className="mb-2">
|
||||
Select from popular scope collections
|
||||
</Header3>
|
||||
<fieldset>
|
||||
@@ -205,60 +207,63 @@ export function ConnectToOAuthForm({
|
||||
variant="button/small"
|
||||
/>
|
||||
</fieldset> */}
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<Header3>Select {integration.name} scopes</Header3>
|
||||
<Paragraph variant="small" className="text-slate-500">
|
||||
{simplur`${selectedScopes.size} scope[|s] selected`}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search scopes"
|
||||
className="mb-2"
|
||||
variant="medium"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="mb-28 flex flex-col gap-y-0.5 overflow-hidden rounded-md">
|
||||
{filteredItems.length === 0 && (
|
||||
<Paragraph variant="small" className="p-4">
|
||||
No scopes match {filterText}. Try a different search query.
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<Header3>Select {integration.name} scopes</Header3>
|
||||
<Paragraph variant="small" className="text-slate-500">
|
||||
{simplur`${selectedScopes.size} scope[|s] selected`}
|
||||
</Paragraph>
|
||||
)}
|
||||
{authMethod.scopes.map((s) => {
|
||||
return (
|
||||
<Checkbox
|
||||
key={s.name}
|
||||
id={s.name}
|
||||
value={s.name}
|
||||
name="scopes"
|
||||
label={s.name}
|
||||
defaultChecked={s.defaultChecked ?? false}
|
||||
badges={s.annotations?.map((a) => a.label)}
|
||||
description={s.description}
|
||||
variant="description"
|
||||
className={cn(
|
||||
filteredItems.find((f) => f.name === s.name) ? "" : "hidden"
|
||||
)}
|
||||
onChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.add(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
} else {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.delete(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search scopes"
|
||||
className="mb-2"
|
||||
variant="medium"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="mb-28 flex flex-col gap-y-0.5 overflow-hidden rounded-md">
|
||||
{filteredItems.length === 0 && (
|
||||
<Paragraph variant="small" className="p-4">
|
||||
No scopes match {filterText}. Try a different search query.
|
||||
</Paragraph>
|
||||
)}
|
||||
{authMethod.scopes.map((s) => {
|
||||
return (
|
||||
<Checkbox
|
||||
key={s.name}
|
||||
id={s.name}
|
||||
value={s.name}
|
||||
name="scopes"
|
||||
label={s.name}
|
||||
defaultChecked={s.defaultChecked ?? false}
|
||||
badges={s.annotations?.map((a) => a.label)}
|
||||
description={s.description}
|
||||
variant="description"
|
||||
className={cn(
|
||||
filteredItems.find((f) => f.name === s.name)
|
||||
? ""
|
||||
: "hidden"
|
||||
)}
|
||||
onChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.add(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
} else {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.delete(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fieldset>
|
||||
|
||||
<div className="absolute bottom-0 left-0 flex w-full items-center justify-end gap-x-4 rounded-b-md border-t border-slate-800 bg-midnight-900 p-4">
|
||||
@@ -268,7 +273,7 @@ export function ConnectToOAuthForm({
|
||||
className="flex gap-2"
|
||||
disabled={transition.state !== "idle"}
|
||||
variant="primary/medium"
|
||||
LeadingIcon={integration.identifier}
|
||||
LeadingIcon={integration.icon ?? integration.identifier}
|
||||
>
|
||||
Connect to {integration.name}
|
||||
</Button>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function CustomHelp({ name }: { name: string }) {
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
code={`
|
||||
new Job(client, {
|
||||
client.defineJob({
|
||||
id: "scheduled-job-1",
|
||||
name: "Scheduled Job 1",
|
||||
version: "0.1.1",
|
||||
@@ -54,7 +54,7 @@ new Job(client, {
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
code={`
|
||||
new Job(client, {
|
||||
client.defineJob({
|
||||
id: "scheduled-job-1",
|
||||
name: "Scheduled Job 1",
|
||||
version: "0.1.1",
|
||||
|
||||
@@ -28,7 +28,7 @@ export function HelpInstall({ packageName }: { packageName: string }) {
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`pnpm install ${packageName}`}
|
||||
value={`pnpm add ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
|
||||
@@ -45,6 +45,7 @@ export function IntegrationWithMissingFieldSheet({
|
||||
to={docsIntegrationPath(integration.identifier)}
|
||||
variant="secondary/small"
|
||||
LeadingIcon="docs"
|
||||
target="_blank"
|
||||
>
|
||||
View docs
|
||||
</LinkButton>
|
||||
|
||||
@@ -172,15 +172,16 @@ export function UpdateOAuthForm({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Header2>Scopes</Header2>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Select the scopes you want to grant to {integration.name} in order
|
||||
for it to access your data. Note: If you try and perform an action
|
||||
in a Job that requires a scope you haven’t granted, that task will
|
||||
fail.
|
||||
</Paragraph>
|
||||
{/* <Header3 className="mb-2">
|
||||
{authMethod.scopes.length > 0 && (
|
||||
<div>
|
||||
<Header2>Scopes</Header2>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Select the scopes you want to grant to {integration.name} in order
|
||||
for it to access your data. Note: If you try and perform an action
|
||||
in a Job that requires a scope you haven’t granted, that task will
|
||||
fail.
|
||||
</Paragraph>
|
||||
{/* <Header3 className="mb-2">
|
||||
Select from popular scope collections
|
||||
</Header3>
|
||||
<fieldset>
|
||||
@@ -190,60 +191,63 @@ export function UpdateOAuthForm({
|
||||
variant="button/small"
|
||||
/>
|
||||
</fieldset> */}
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<Header3>Select {integration.name} scopes</Header3>
|
||||
<Paragraph variant="small" className="text-slate-500">
|
||||
{simplur`${selectedScopes.size} scope[|s] selected`}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search scopes"
|
||||
className="mb-2"
|
||||
variant="medium"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="mb-28 flex flex-col gap-y-0.5 overflow-hidden rounded-md">
|
||||
{filteredItems.length === 0 && (
|
||||
<Paragraph variant="small" className="p-4">
|
||||
No scopes match {filterText}. Try a different search query.
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<Header3>Select {integration.name} scopes</Header3>
|
||||
<Paragraph variant="small" className="text-slate-500">
|
||||
{simplur`${selectedScopes.size} scope[|s] selected`}
|
||||
</Paragraph>
|
||||
)}
|
||||
{authMethod.scopes.map((s) => {
|
||||
return (
|
||||
<Checkbox
|
||||
key={s.name}
|
||||
id={s.name}
|
||||
value={s.name}
|
||||
name="scopes"
|
||||
label={s.name}
|
||||
defaultChecked={s.defaultChecked ?? false}
|
||||
badges={s.annotations?.map((a) => a.label)}
|
||||
description={s.description}
|
||||
variant="description"
|
||||
className={cn(
|
||||
filteredItems.find((f) => f.name === s.name) ? "" : "hidden"
|
||||
)}
|
||||
onChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.add(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
} else {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.delete(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search scopes"
|
||||
className="mb-2"
|
||||
variant="medium"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="mb-28 flex flex-col gap-y-0.5 overflow-hidden rounded-md">
|
||||
{filteredItems.length === 0 && (
|
||||
<Paragraph variant="small" className="p-4">
|
||||
No scopes match {filterText}. Try a different search query.
|
||||
</Paragraph>
|
||||
)}
|
||||
{authMethod.scopes.map((s) => {
|
||||
return (
|
||||
<Checkbox
|
||||
key={s.name}
|
||||
id={s.name}
|
||||
value={s.name}
|
||||
name="scopes"
|
||||
label={s.name}
|
||||
defaultChecked={s.defaultChecked ?? false}
|
||||
badges={s.annotations?.map((a) => a.label)}
|
||||
description={s.description}
|
||||
variant="description"
|
||||
className={cn(
|
||||
filteredItems.find((f) => f.name === s.name)
|
||||
? ""
|
||||
: "hidden"
|
||||
)}
|
||||
onChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.add(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
} else {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.delete(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fieldset>
|
||||
|
||||
<div className="absolute bottom-0 left-0 flex w-full items-center justify-end gap-x-4 rounded-b-md border-t border-slate-800 bg-midnight-900 p-4">
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
} from "../primitives/Table";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
import { runStatusTitle } from "../runs/RunStatuses";
|
||||
import { ProjectJob, useProject } from "~/hooks/useProject";
|
||||
import { ProjectJob } from "~/hooks/useJobs";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { JobRunStatus } from "~/models/job.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
@@ -1,218 +1,29 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { Fragment } from "react";
|
||||
import {
|
||||
useIntegrationClient,
|
||||
useOptionalIntegrationClient,
|
||||
} from "~/hooks/useIntegrationClient";
|
||||
import { useJob, useOptionalJob } from "~/hooks/useJob";
|
||||
import {
|
||||
useOptionalOrganization,
|
||||
useOrganization,
|
||||
} from "~/hooks/useOrganizations";
|
||||
import { useOptionalProject, useProject } from "~/hooks/useProject";
|
||||
import { useOptionalRun, useRun } from "~/hooks/useRun";
|
||||
import {
|
||||
integrationClientConnectionsPath,
|
||||
integrationClientPath,
|
||||
integrationClientScopesPath,
|
||||
jobPath,
|
||||
jobTestPath,
|
||||
projectEnvironmentsPath,
|
||||
projectIntegrationsPath,
|
||||
projectPath,
|
||||
runDashboardPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { RouteMatch, useMatches } from "@remix-run/react";
|
||||
import { Fragment, ReactNode } from "react";
|
||||
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
|
||||
import { JobsMenu } from "./JobsMenu";
|
||||
import { BreadcrumbLink } from "./NavBar";
|
||||
import { ProjectsMenu } from "./ProjectsMenu";
|
||||
|
||||
export type Breadcrumb = {
|
||||
slug:
|
||||
| "projects"
|
||||
| "jobs"
|
||||
| "integrations"
|
||||
| "integration"
|
||||
| "integration-job"
|
||||
| "integration-connections"
|
||||
| "integration-scopes"
|
||||
| "environments"
|
||||
| "job"
|
||||
| "test"
|
||||
| "runs"
|
||||
| "run";
|
||||
link?: {
|
||||
to: string;
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
function useBreadcrumbs(): Breadcrumb[] {
|
||||
const matches = useMatches();
|
||||
|
||||
return matches
|
||||
.filter((match) => match.handle)
|
||||
.filter((match) => match.handle!.breadcrumb)
|
||||
.map((match) => match.handle!.breadcrumb as Breadcrumb);
|
||||
}
|
||||
export type BreadcrumbItem = (
|
||||
match: RouteMatch,
|
||||
allMatches: RouteMatch[]
|
||||
) => ReactNode;
|
||||
|
||||
export function Breadcrumb() {
|
||||
const breadcrumbs = useBreadcrumbs();
|
||||
const organization = useOptionalOrganization();
|
||||
const project = useOptionalProject();
|
||||
const job = useOptionalJob();
|
||||
const run = useOptionalRun();
|
||||
const client = useOptionalIntegrationClient();
|
||||
const matches = useMatches();
|
||||
|
||||
return (
|
||||
<div className="hidden items-center md:flex">
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbIcon />
|
||||
<BreadcrumbItem
|
||||
breadcrumb={breadcrumb}
|
||||
organization={organization}
|
||||
project={project}
|
||||
job={job}
|
||||
run={run}
|
||||
client={client}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
{matches.map((match) => {
|
||||
if (!match.handle || !match.handle.breadcrumb) return null;
|
||||
|
||||
const breadcrumb = match.handle.breadcrumb as BreadcrumbItem;
|
||||
|
||||
return (
|
||||
<Fragment key={match.id}>
|
||||
<BreadcrumbIcon />
|
||||
{breadcrumb(match, matches)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({
|
||||
breadcrumb,
|
||||
organization,
|
||||
project,
|
||||
job,
|
||||
run,
|
||||
client,
|
||||
}: {
|
||||
breadcrumb: Breadcrumb;
|
||||
organization?: ReturnType<typeof useOrganization>;
|
||||
project?: ReturnType<typeof useProject>;
|
||||
job?: ReturnType<typeof useJob>;
|
||||
run?: ReturnType<typeof useRun>;
|
||||
client?: ReturnType<typeof useIntegrationClient>;
|
||||
}) {
|
||||
switch (breadcrumb.slug) {
|
||||
case "projects":
|
||||
return <ProjectsMenu key={breadcrumb.slug} />;
|
||||
case "jobs":
|
||||
if (!organization || !project) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectPath(organization, project)}
|
||||
title="Jobs"
|
||||
/>
|
||||
);
|
||||
case "environments":
|
||||
if (!organization || !project) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectEnvironmentsPath(organization, project)}
|
||||
title="Environments"
|
||||
/>
|
||||
);
|
||||
case "integrations":
|
||||
if (!organization || !project) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
title="Integrations"
|
||||
/>
|
||||
);
|
||||
case "integration":
|
||||
if (!organization || !project || !client) return null;
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
title="Integrations"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<BreadcrumbLink
|
||||
to={integrationClientPath(organization, project, client)}
|
||||
title={client.title}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
case "integration-job":
|
||||
if (!organization || !project || !client) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientPath(organization, project, client)}
|
||||
title={"Jobs"}
|
||||
/>
|
||||
);
|
||||
case "integration-connections":
|
||||
if (!organization || !project || !client) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientConnectionsPath(organization, project, client)}
|
||||
title={"Connections"}
|
||||
/>
|
||||
);
|
||||
case "integration-scopes":
|
||||
if (!organization || !project || !client) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
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)}
|
||||
title="Jobs"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<JobsMenu key={breadcrumb.slug} />
|
||||
</Fragment>
|
||||
);
|
||||
case "test":
|
||||
if (!organization || !project || !job) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
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)}
|
||||
title="Runs"
|
||||
/>
|
||||
);
|
||||
case "run":
|
||||
if (!organization || !project || !job || !run) return null;
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={jobPath(organization, project, job)}
|
||||
title="Runs"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<BreadcrumbLink
|
||||
to={runDashboardPath(organization, project, job, run)}
|
||||
title={`Run #${run.number}`}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="text-red-500">{breadcrumb.slug}</span>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Link, RouteMatch } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useJobs } from "~/hooks/useJobs";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { jobPath } from "~/utils/pathBuilder";
|
||||
import { LabelValueStack } from "../primitives/LabelValueStack";
|
||||
@@ -14,11 +15,12 @@ import {
|
||||
PopoverSectionHeader,
|
||||
} from "../primitives/Popover";
|
||||
|
||||
export function JobsMenu() {
|
||||
export function JobsMenu({ matches }: { matches: RouteMatch[] }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const currentJob = useJob();
|
||||
const organization = useOrganization(matches);
|
||||
const project = useProject(matches);
|
||||
const projectJobs = useJobs(matches);
|
||||
const currentJob = useJob(matches);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,7 +35,7 @@ export function JobsMenu() {
|
||||
>
|
||||
<PopoverSectionHeader title="Jobs" />
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
{project.jobs.map((job) => {
|
||||
{projectJobs.map((job) => {
|
||||
const isSelected = job.id === currentJob?.id;
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
BookOpenIcon,
|
||||
ChatBubbleLeftRightIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Fragment } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { LogoIcon } from "../LogoIcon";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { Breadcrumb } from "./Breadcrumb";
|
||||
import { docsRoot } from "~/utils/pathBuilder";
|
||||
|
||||
@@ -27,7 +30,17 @@ export function NavBar() {
|
||||
>
|
||||
Documentation
|
||||
</LinkButton>
|
||||
<Feedback />
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
LeadingIcon={ChatBubbleLeftRightIcon}
|
||||
shortcut={{ key: "f" }}
|
||||
>
|
||||
Help & feedback
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { motion } from "framer-motion";
|
||||
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";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
accountPath,
|
||||
organizationBillingPath,
|
||||
@@ -12,10 +12,11 @@ import {
|
||||
projectEnvironmentsPath,
|
||||
projectIntegrationsPath,
|
||||
projectPath,
|
||||
projectTriggersPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { NavLinkButton } from "../primitives/Buttons";
|
||||
import type { IconNames } from "../primitives/NamedIcon";
|
||||
import { NamedIcon, type IconNames } from "../primitives/NamedIcon";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
|
||||
export function SideMenuContainer({ children }: { children: React.ReactNode }) {
|
||||
@@ -45,14 +46,15 @@ export function ProjectSideMenu() {
|
||||
const project = useProject();
|
||||
const matches = useMatches();
|
||||
|
||||
//we collapse the menu if we're in a job or an integration
|
||||
const job = useOptionalJob();
|
||||
const client = useOptionalIntegrationClient();
|
||||
const isCollapsed = job !== undefined || client !== undefined;
|
||||
//the deepest route `handle` determines if the menu is expanded
|
||||
const deepestMatch = matches.at(-1);
|
||||
const handle = deepestMatch?.handle as Handle;
|
||||
const isCollapsed = handle?.expandSidebar ? !handle.expandSidebar : true;
|
||||
|
||||
const job = useOptionalJob();
|
||||
const jobsActive =
|
||||
job !== undefined ||
|
||||
matches.at(-1)?.id ===
|
||||
deepestMatch?.id ===
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam._index";
|
||||
|
||||
return (
|
||||
@@ -78,8 +80,17 @@ export function ProjectSideMenu() {
|
||||
icon="integration"
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
hasWarning={project.hasUnconfiguredIntegrations}
|
||||
data-action="integrations"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Triggers"
|
||||
icon="trigger"
|
||||
to={projectTriggersPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
hasWarning={project.hasInactiveExternalTriggers}
|
||||
data-action="triggers"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Environments & API Keys"
|
||||
icon="environment"
|
||||
@@ -130,11 +141,13 @@ function SideMenuItem({
|
||||
to,
|
||||
isCollapsed,
|
||||
forceActive,
|
||||
hasWarning = false,
|
||||
}: {
|
||||
icon: IconNames | React.ComponentType<any>;
|
||||
name: string;
|
||||
to: string;
|
||||
isCollapsed: boolean;
|
||||
hasWarning?: boolean;
|
||||
forceActive?: boolean;
|
||||
}) {
|
||||
return (
|
||||
@@ -152,6 +165,7 @@ function SideMenuItem({
|
||||
isActive = forceActive;
|
||||
}
|
||||
return cn(
|
||||
"relative",
|
||||
isActive
|
||||
? "bg-slate-800 text-bright group-hover:bg-slate-800"
|
||||
: "text-dimmed group-hover:bg-slate-850 group-hover:text-bright"
|
||||
@@ -166,6 +180,9 @@ function SideMenuItem({
|
||||
>
|
||||
{name}
|
||||
</motion.span>
|
||||
{hasWarning && (
|
||||
<NamedIcon name="error" className="absolute left-1 top-1 h-4 w-4" />
|
||||
)}
|
||||
</NavLinkButton>
|
||||
}
|
||||
content={name}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { Fragment, useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import {
|
||||
useIsNewOrganizationPage,
|
||||
useOrganizations,
|
||||
@@ -17,11 +20,11 @@ import {
|
||||
PopoverSectionHeader,
|
||||
} from "../primitives/Popover";
|
||||
|
||||
export function ProjectsMenu() {
|
||||
export function ProjectsMenu({ matches }: { matches: RouteMatch[] }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const organizations = useOrganizations();
|
||||
const isNewOrgPage = useIsNewOrganizationPage();
|
||||
const currentProject = useOptionalProject();
|
||||
const organizations = useOrganizations(matches);
|
||||
const isNewOrgPage = useIsNewOrganizationPage(matches);
|
||||
const currentProject = useOptionalProject(matches);
|
||||
|
||||
if (isNewOrgPage) {
|
||||
return null;
|
||||
@@ -48,7 +51,14 @@ export function ProjectsMenu() {
|
||||
<PopoverMenuItem
|
||||
key={project.id}
|
||||
to={projectPath(organization, project)}
|
||||
title={project.name}
|
||||
title={
|
||||
<div className="flex w-full items-center justify-between pl-1 text-bright">
|
||||
<span className="grow truncate text-left">
|
||||
{project.name}
|
||||
</span>
|
||||
<Badge className="mr-0.5">{simplur`${project._count.jobs} job[|s]`}</Badge>
|
||||
</div>
|
||||
}
|
||||
isSelected={isSelected}
|
||||
icon="folder"
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { cn } from "~/utils/cn";
|
||||
const variants = {
|
||||
default:
|
||||
"grid place-items-center rounded-full px-2 h-5 tracking-wider text-xxs bg-slate-700 text-bright uppercase whitespace-nowrap",
|
||||
outline:
|
||||
"grid place-items-center rounded-sm px-1 h-5 tracking-wider text-xxs border border-dimmed text-dimmed uppercase whitespace-nowrap",
|
||||
};
|
||||
|
||||
type BadgeProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, {
|
||||
} from "react";
|
||||
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
|
||||
const variant = {
|
||||
@@ -125,8 +125,8 @@ const allVariants = {
|
||||
|
||||
type ButtonContentPropsType = {
|
||||
children?: React.ReactNode;
|
||||
LeadingIcon?: React.ComponentType<any> | string;
|
||||
TrailingIcon?: React.ComponentType<any> | string;
|
||||
LeadingIcon?: React.ComponentType<any> | IconNamesOrString;
|
||||
TrailingIcon?: React.ComponentType<any> | IconNamesOrString;
|
||||
trailingIconClassName?: string;
|
||||
leadingIconClassName?: string;
|
||||
fullWidth?: boolean;
|
||||
|
||||
@@ -24,16 +24,14 @@ export const variantClasses = {
|
||||
warning: {
|
||||
className: "border-yellow-400/20 bg-yellow-400/30",
|
||||
icon: (
|
||||
<ExclamationCircleIcon className="h-5 w-5 shrink-0 text-yellow-400" />
|
||||
<ExclamationTriangleIcon className="h-5 w-5 shrink-0 text-yellow-400" />
|
||||
),
|
||||
textColor: "text-yellow-200",
|
||||
linkClassName: "transition hover:bg-yellow-400/40",
|
||||
},
|
||||
error: {
|
||||
className: "border-rose-500/20 bg-rose-500/30",
|
||||
icon: (
|
||||
<ExclamationTriangleIcon className="h-5 w-5 shrink-0 text-rose-400" />
|
||||
),
|
||||
icon: <ExclamationCircleIcon className="h-5 w-5 shrink-0 text-rose-400" />,
|
||||
textColor: "text-rose-200",
|
||||
linkClassName: "transition hover:bg-rose-500/40",
|
||||
},
|
||||
|
||||
@@ -106,11 +106,11 @@ export function ClipboardField({
|
||||
const inputIcon = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
<span
|
||||
className={cn(container, fullWidth ? "w-full" : "max-w-max", className)}
|
||||
>
|
||||
{icon && (
|
||||
<div
|
||||
<span
|
||||
onClick={() => inputIcon.current && inputIcon.current.focus()}
|
||||
className={cn(iconPosition, "flex items-center")}
|
||||
>
|
||||
@@ -119,7 +119,7 @@ export function ClipboardField({
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
@@ -153,6 +153,6 @@ export function ClipboardField({
|
||||
>
|
||||
{copied ? <CheckIcon className="h-4 w-4 text-green-500" /> : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,96 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const header1Styles =
|
||||
"font-sans text-base md:text-lg lg:text-xl leading-5 md:leading-6 lg:leading-7 font-semibold";
|
||||
const header2Styles = "font-sans text-base leading-6 font-medium text-bright";
|
||||
const header3Styles = "font-sans text-sm leading-5 font-medium text-bright";
|
||||
const headerVariants = {
|
||||
header1: {
|
||||
text: "font-sans text-base md:text-lg lg:text-xl leading-5 md:leading-6 lg:leading-7 font-semibold",
|
||||
spacing: "mb-2",
|
||||
},
|
||||
header2: {
|
||||
text: "font-sans text-base leading-6 font-medium",
|
||||
spacing: "mb-2",
|
||||
},
|
||||
header3: {
|
||||
text: "font-sans text-sm leading-5 font-medium",
|
||||
spacing: "mb-2",
|
||||
},
|
||||
};
|
||||
|
||||
const header1Variants = {
|
||||
default: "text-bright",
|
||||
const textColorVariants = {
|
||||
bright: "text-bright",
|
||||
dimmed: "text-dimmed",
|
||||
};
|
||||
|
||||
export type HeaderVariant = keyof typeof headerVariants;
|
||||
|
||||
type HeaderProps = {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type Header1Props = HeaderProps & {
|
||||
variant?: keyof typeof header1Variants;
|
||||
};
|
||||
spacing?: boolean;
|
||||
textColor?: "bright" | "dimmed";
|
||||
} & React.HTMLAttributes<HTMLHeadingElement>;
|
||||
|
||||
export function Header1({
|
||||
variant = "default",
|
||||
className,
|
||||
children,
|
||||
}: Header1Props) {
|
||||
spacing = false,
|
||||
textColor = "bright",
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<h1 className={cn(header1Styles, header1Variants[variant], className)}>
|
||||
<h1
|
||||
className={cn(
|
||||
headerVariants.header1.text,
|
||||
spacing === true && headerVariants.header1.spacing,
|
||||
textColor === "bright"
|
||||
? textColorVariants.bright
|
||||
: textColorVariants.dimmed,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header2({ className, children }: HeaderProps) {
|
||||
return <h2 className={cn(header2Styles, className)}>{children}</h2>;
|
||||
export function Header2({
|
||||
className,
|
||||
children,
|
||||
spacing = false,
|
||||
textColor = "bright",
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<h2
|
||||
className={cn(
|
||||
headerVariants.header2.text,
|
||||
spacing === true && headerVariants.header2.spacing,
|
||||
textColor === "bright"
|
||||
? textColorVariants.bright
|
||||
: textColorVariants.dimmed,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header3({ className, children }: HeaderProps) {
|
||||
return <h3 className={cn(header3Styles, className)}>{children}</h3>;
|
||||
export function Header3({
|
||||
className,
|
||||
children,
|
||||
spacing = false,
|
||||
textColor = "bright",
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<h3
|
||||
className={cn(
|
||||
headerVariants.header3.text,
|
||||
spacing === true && headerVariants.header3.spacing,
|
||||
textColor === "bright"
|
||||
? textColorVariants.bright
|
||||
: textColorVariants.dimmed,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Header2, Header3 } from "./Headers";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
const labelVariants = {
|
||||
small: {
|
||||
text: "font-sans text-sm font-normal",
|
||||
},
|
||||
medium: {
|
||||
text: "font-sans text-sm leading-5 font-medium",
|
||||
},
|
||||
large: {
|
||||
text: "font-sans text-base leading-6 font-medium",
|
||||
},
|
||||
};
|
||||
|
||||
type LabelProps = React.AllHTMLAttributes<HTMLLabelElement> & {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
variant?: "small" | "medium" | "large";
|
||||
variant?: keyof typeof labelVariants;
|
||||
};
|
||||
|
||||
export function Label({
|
||||
@@ -15,17 +25,10 @@ export function Label({
|
||||
variant = "medium",
|
||||
...props
|
||||
}: LabelProps) {
|
||||
return variant === "medium" ? (
|
||||
<Header3 className={cn(className)} {...props}>
|
||||
const variation = labelVariants[variant];
|
||||
return (
|
||||
<label className={cn(variation.text, className)} {...props}>
|
||||
{children}
|
||||
</Header3>
|
||||
) : variant === "large" ? (
|
||||
<Header2 className={cn(className)} {...props}>
|
||||
{children}
|
||||
</Header2>
|
||||
) : (
|
||||
<Paragraph variant="small" className={cn(className)}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import { Paragraph, ParagraphVariant } from "./Paragraph";
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
|
||||
import { SimpleTooltip } from "./Tooltip";
|
||||
|
||||
const variations: Record<
|
||||
string,
|
||||
{ label: ParagraphVariant; value: ParagraphVariant }
|
||||
> = {
|
||||
const variations = {
|
||||
primary: {
|
||||
label: "extra-small/bright",
|
||||
value: "extra-small",
|
||||
@@ -15,7 +12,7 @@ const variations: Record<
|
||||
label: "extra-extra-small/caps",
|
||||
value: "extra-small/bright",
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
type LabelValueStackProps = {
|
||||
label: React.ReactNode;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
StopIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowPathIcon,
|
||||
@@ -17,6 +15,7 @@ import {
|
||||
BuildingOffice2Icon,
|
||||
CalendarDaysIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
@@ -47,20 +46,26 @@ import {
|
||||
UserIcon,
|
||||
UserPlusIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
XCircleIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
|
||||
import { HourglassIcon } from "lucide-react";
|
||||
import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon";
|
||||
import { ErrorIcon } from "~/assets/icons/ErrorIcon";
|
||||
import { ScheduleIcon } from "~/assets/icons/ScheduleIcon";
|
||||
import { WebhookIcon } from "~/assets/icons/WebhookIcon";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { LogoIcon } from "../LogoIcon";
|
||||
import { Spinner } from "./Spinner";
|
||||
import { HourglassIcon } from "lucide-react";
|
||||
import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon";
|
||||
|
||||
const icons = {
|
||||
account: (className: string) => (
|
||||
<UserCircleIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
active: (className: string) => (
|
||||
<CheckCircleIcon className={cn("text-green-500", className)} />
|
||||
),
|
||||
"arrow-right": (className: string) => (
|
||||
<ArrowRightIcon className={cn("text-white", className)} />
|
||||
),
|
||||
@@ -113,7 +118,7 @@ const icons = {
|
||||
<DynamicTriggerIcon className={cn("text-cyan-500", className)} />
|
||||
),
|
||||
error: (className: string) => (
|
||||
<ExclamationCircleIcon className={cn("text-rose-500", className)} />
|
||||
<ErrorIcon className={cn("text-rose-500", className)} />
|
||||
),
|
||||
"external-link": (className: string) => (
|
||||
<ArrowTopRightOnSquareIcon className={cn("text-dimmed", className)} />
|
||||
@@ -142,6 +147,9 @@ const icons = {
|
||||
id: (className: string) => (
|
||||
<FingerPrintIcon className={cn("text-rose-200", className)} />
|
||||
),
|
||||
inactive: (className: string) => (
|
||||
<XCircleIcon className={cn("text-rose-500", className)} />
|
||||
),
|
||||
info: (className: string) => (
|
||||
<InformationCircleIcon className={cn("text-blue-500", className)} />
|
||||
),
|
||||
@@ -197,6 +205,9 @@ const icons = {
|
||||
star: (className: string) => (
|
||||
<StarIcon className={cn("text-yellow-500", className)} />
|
||||
),
|
||||
stop: (className: string) => (
|
||||
<StopIcon className={cn("text-rose-500", className)} />
|
||||
),
|
||||
team: (className: string) => (
|
||||
<UserGroupIcon className={cn("text-blue-500", className)} />
|
||||
),
|
||||
@@ -213,8 +224,11 @@ const icons = {
|
||||
"custom-event": (className: string) => (
|
||||
<CodeBracketSquareIcon className={cn("text-toxic-600", className)} />
|
||||
),
|
||||
"register-source": (className: string) => (
|
||||
<GlobeAltIcon className={cn("text-sky-500", className)} />
|
||||
),
|
||||
"schedule-interval": (className: string) => (
|
||||
<ScheduleIcon className={cn("text-sky-500", className)} />
|
||||
<ClockIcon className={cn("text-sky-500", className)} />
|
||||
),
|
||||
"schedule-cron": (className: string) => (
|
||||
<ScheduleIcon className={cn("text-sky-500", className)} />
|
||||
@@ -278,10 +292,12 @@ export function NamedIconInBox({
|
||||
name,
|
||||
className,
|
||||
fallback,
|
||||
iconClassName,
|
||||
}: {
|
||||
name: string;
|
||||
className?: string;
|
||||
fallback?: JSX.Element;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -290,7 +306,11 @@ export function NamedIconInBox({
|
||||
className
|
||||
)}
|
||||
>
|
||||
<NamedIcon name={name} fallback={fallback} className="h-6 w-6" />
|
||||
<NamedIcon
|
||||
name={name}
|
||||
fallback={fallback}
|
||||
className={cn("h-6 w-6", iconClassName)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
|
||||
>
|
||||
<ChevronLeftIcon className="h-6" />
|
||||
<Header1
|
||||
variant="dimmed"
|
||||
textColor="dimmed"
|
||||
className="transition group-hover:text-white"
|
||||
>
|
||||
{backButton.text}
|
||||
|
||||
@@ -90,49 +90,3 @@ export function Paragraph({
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
type TextLinkProps = {
|
||||
href?: string;
|
||||
to?: string;
|
||||
className?: string;
|
||||
trailingIcon?: IconNamesOrString;
|
||||
trailingIconClassName?: string;
|
||||
children: React.ReactNode;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function StepNumber({
|
||||
stepNumber?: string;
|
||||
active?: boolean;
|
||||
complete?: boolean;
|
||||
title?: string;
|
||||
title?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -4,14 +4,20 @@ import { ReactNode, forwardRef } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
type TableProps = {
|
||||
containerClassName?: string;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const Table = forwardRef<HTMLTableElement, TableProps>(
|
||||
({ className, children }, ref) => {
|
||||
({ className, containerClassName, children }, ref) => {
|
||||
return (
|
||||
<div className="overflow-x-auto whitespace-nowrap rounded-md border border-slate-900 scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700">
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-x-auto whitespace-nowrap rounded-md border border-slate-900 scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full divide-y bg-midnight-850", className)}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const variations = {
|
||||
primary:
|
||||
"text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center group",
|
||||
secondary:
|
||||
"text-dimmed transition underline underline-offset-2 decoration-dimmed/50 hover:decoration-dimmed inline-flex gap-0.5 items-center group",
|
||||
} as const;
|
||||
|
||||
type TextLinkProps = {
|
||||
href?: string;
|
||||
to?: string;
|
||||
className?: string;
|
||||
trailingIcon?: IconNamesOrString;
|
||||
trailingIconClassName?: string;
|
||||
variant?: keyof typeof variations;
|
||||
children: React.ReactNode;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||
|
||||
export function TextLink({
|
||||
href,
|
||||
to,
|
||||
children,
|
||||
className,
|
||||
trailingIcon,
|
||||
trailingIconClassName,
|
||||
variant = "primary",
|
||||
...props
|
||||
}: TextLinkProps) {
|
||||
const classes = variations[variant];
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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 { MatchedRun, useRun } from "~/hooks/useRun";
|
||||
import { formatDuration } from "~/utils";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelDivider,
|
||||
RunPanelError,
|
||||
RunPanelHeader,
|
||||
RunPanelIconProperty,
|
||||
RunPanelIconSection,
|
||||
} from "./RunCard";
|
||||
|
||||
export function RunCompletedDetail({ run }: { run: MatchedRun }) {
|
||||
return (
|
||||
<RunPanel>
|
||||
<RunPanelHeader
|
||||
icon={<RunStatusIcon status={run.status} className={"h-5 w-5"} />}
|
||||
title={
|
||||
<Paragraph variant="small/bright">
|
||||
<RunStatusLabel status={run.status} />
|
||||
</Paragraph>
|
||||
}
|
||||
/>
|
||||
<RunPanelBody>
|
||||
<RunPanelIconSection>
|
||||
{run.startedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Started at"
|
||||
value={<DateTime date={run.startedAt} />}
|
||||
/>
|
||||
)}
|
||||
{run.completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={<DateTime date={run.completedAt} />}
|
||||
/>
|
||||
)}
|
||||
{run.startedAt && run.completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="clock"
|
||||
label="Total duration"
|
||||
value={formatDuration(run.startedAt, run.completedAt, {
|
||||
style: "long",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</RunPanelIconSection>
|
||||
<RunPanelDivider />
|
||||
{run.error && (
|
||||
<RunPanelError
|
||||
text={run.error.message}
|
||||
stackTrace={run.error.stack}
|
||||
/>
|
||||
)}
|
||||
{run.output ? (
|
||||
<CodeBlock language="json" code={run.output} />
|
||||
) : (
|
||||
run.output === null && (
|
||||
<Paragraph variant="small">This run returned nothing</Paragraph>
|
||||
)
|
||||
)}
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { BoltIcon, ForwardIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
Form,
|
||||
Outlet,
|
||||
useActionData,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useNavigation,
|
||||
} from "@remix-run/react";
|
||||
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { useMemo } from "react";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { Run } from "~/presenters/RunPresenter.server";
|
||||
import { cancelSchema } from "~/routes/resources.runs.$runId.cancel";
|
||||
import { schema } from "~/routes/resources.runs.$runId.rerun";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
runCompletedPath,
|
||||
runTaskPath,
|
||||
runTriggerPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "../layout/AppLayout";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import {
|
||||
PageButtons,
|
||||
PageHeader,
|
||||
PageInfoGroup,
|
||||
PageInfoProperty,
|
||||
PageInfoRow,
|
||||
PageTitle,
|
||||
PageTitleRow,
|
||||
} from "../primitives/PageHeader";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
|
||||
import {
|
||||
RunBasicStatus,
|
||||
RunStatusIcon,
|
||||
RunStatusLabel,
|
||||
hasFinished,
|
||||
runBasicStatus,
|
||||
runStatusTitle,
|
||||
} from "../runs/RunStatuses";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelDivider,
|
||||
RunPanelError,
|
||||
RunPanelHeader,
|
||||
RunPanelIconProperty,
|
||||
RunPanelIconSection,
|
||||
RunPanelProperties,
|
||||
} from "./RunCard";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
import { TaskCardSkeleton } from "./TaskCardSkeleton";
|
||||
|
||||
type RunOverviewProps = {
|
||||
run: Run;
|
||||
trigger: {
|
||||
icon: string;
|
||||
title: string;
|
||||
};
|
||||
showRerun: boolean;
|
||||
paths: {
|
||||
back: string;
|
||||
run: string;
|
||||
runsPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
const taskPattern = /\/tasks\/(.*)/;
|
||||
|
||||
export function RunOverview({
|
||||
run,
|
||||
trigger,
|
||||
showRerun,
|
||||
paths,
|
||||
}: RunOverviewProps) {
|
||||
const navigate = useNavigate();
|
||||
const pathName = usePathName();
|
||||
|
||||
const selectedId = useMemo(() => {
|
||||
if (pathName.endsWith("/completed")) {
|
||||
return "completed";
|
||||
}
|
||||
|
||||
if (pathName.endsWith("/trigger")) {
|
||||
return "trigger";
|
||||
}
|
||||
|
||||
const taskMatch = pathName.match(taskPattern);
|
||||
const taskId = taskMatch ? taskMatch[1] : undefined;
|
||||
if (taskId) {
|
||||
return taskId;
|
||||
}
|
||||
}, [pathName]);
|
||||
|
||||
const basicStatus = runBasicStatus(run.status);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitleRow>
|
||||
<PageTitle
|
||||
backButton={{
|
||||
to: paths.back,
|
||||
text: "Runs",
|
||||
}}
|
||||
title={`Run #${run.number}`}
|
||||
/>
|
||||
<PageButtons>
|
||||
{run.isTest && (
|
||||
<span className="flex items-center gap-1 text-xs uppercase text-slate-600">
|
||||
<NamedIcon name="beaker" className="h-4 w-4 text-slate-600" />
|
||||
Test run
|
||||
</span>
|
||||
)}
|
||||
{showRerun && hasFinished(run.status) && (
|
||||
<RerunPopover
|
||||
runId={run.id}
|
||||
runsPath={paths.runsPath}
|
||||
environmentType={run.environment.type}
|
||||
status={basicStatus}
|
||||
/>
|
||||
)}
|
||||
{!hasFinished(run.status) && <CancelRun runId={run.id} />}
|
||||
</PageButtons>
|
||||
</PageTitleRow>
|
||||
<PageInfoRow>
|
||||
<PageInfoGroup>
|
||||
<PageInfoProperty
|
||||
icon={<RunStatusIcon status={run.status} className="h-4 w-4" />}
|
||||
label={"Status"}
|
||||
value={runStatusTitle(run.status)}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"calendar"}
|
||||
label={"Started"}
|
||||
value={
|
||||
run.startedAt ? (
|
||||
<DateTime date={run.startedAt} />
|
||||
) : (
|
||||
"Not started yet"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"property"}
|
||||
label={"Version"}
|
||||
value={`v${run.version}`}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
label={"Env"}
|
||||
value={<EnvironmentLabel environment={run.environment} />}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"clock"}
|
||||
label={"Duration"}
|
||||
value={formatDuration(run.startedAt, run.completedAt)}
|
||||
/>
|
||||
</PageInfoGroup>
|
||||
<PageInfoGroup alignment="right">
|
||||
<Paragraph variant="extra-small" className="text-slate-600">
|
||||
RUN ID: {run.id}
|
||||
</Paragraph>
|
||||
</PageInfoGroup>
|
||||
</PageInfoRow>
|
||||
</PageHeader>
|
||||
<PageBody scrollable={false}>
|
||||
<div className="grid h-full grid-cols-2 gap-2">
|
||||
<div className="flex flex-col gap-6 overflow-y-auto py-4 pl-4 pr-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<div>
|
||||
<Header2 className="mb-2">Trigger</Header2>
|
||||
<RunPanel
|
||||
selected={selectedId === "trigger"}
|
||||
onClick={() => navigate(runTriggerPath(paths.run))}
|
||||
>
|
||||
<RunPanelHeader icon={trigger.icon} title={trigger.title} />
|
||||
<RunPanelBody>
|
||||
<RunPanelProperties
|
||||
properties={[
|
||||
{ label: "Event name", text: run.event.name },
|
||||
...run.properties,
|
||||
]}
|
||||
/>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
</div>
|
||||
<div>
|
||||
<Header2 className="mb-2">Tasks</Header2>
|
||||
|
||||
{run.tasks.length > 0 ? (
|
||||
run.tasks.map((task, index) => {
|
||||
const isLast = index === run.tasks.length - 1;
|
||||
|
||||
return (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
selectedId={selectedId}
|
||||
selectedTask={(taskId) => {
|
||||
navigate(runTaskPath(paths.run, taskId));
|
||||
}}
|
||||
isLast={isLast}
|
||||
depth={0}
|
||||
{...task}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<BlankTasks status={run.status} basicStatus={basicStatus} />
|
||||
)}
|
||||
</div>
|
||||
{(basicStatus === "COMPLETED" || basicStatus === "FAILED") && (
|
||||
<div>
|
||||
<Header2 className={cn("mb-2")}>Run Summary</Header2>
|
||||
<RunPanel
|
||||
selected={selectedId === "completed"}
|
||||
onClick={() => navigate(runCompletedPath(paths.run))}
|
||||
>
|
||||
<RunPanelHeader
|
||||
icon={
|
||||
<RunStatusIcon
|
||||
status={run.status}
|
||||
className={"h-5 w-5"}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<Paragraph variant="small/bright">
|
||||
<RunStatusLabel status={run.status} />
|
||||
</Paragraph>
|
||||
}
|
||||
/>
|
||||
<RunPanelBody>
|
||||
<RunPanelIconSection>
|
||||
{run.startedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Started at"
|
||||
value={<DateTime date={run.startedAt} />}
|
||||
/>
|
||||
)}
|
||||
{run.completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={<DateTime date={run.completedAt} />}
|
||||
/>
|
||||
)}
|
||||
{run.startedAt && run.completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="clock"
|
||||
label="Total duration"
|
||||
value={formatDuration(
|
||||
run.startedAt,
|
||||
run.completedAt,
|
||||
{
|
||||
style: "long",
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</RunPanelIconSection>
|
||||
<RunPanelDivider />
|
||||
{run.error && (
|
||||
<RunPanelError
|
||||
text={run.error.message}
|
||||
stackTrace={run.error.stack}
|
||||
/>
|
||||
)}
|
||||
{run.output ? (
|
||||
<CodeBlock
|
||||
language="json"
|
||||
code={run.output}
|
||||
maxLines={10}
|
||||
/>
|
||||
) : (
|
||||
run.output === null && (
|
||||
<Paragraph variant="small">
|
||||
This Run returned nothing.
|
||||
</Paragraph>
|
||||
)
|
||||
)}
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detail view */}
|
||||
<div className="overflow-y-auto py-4 pr-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<Header2 className="mb-2">Detail</Header2>
|
||||
{selectedId ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<Callout variant="info">Select a task or trigger</Callout>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function BlankTasks({
|
||||
status,
|
||||
basicStatus,
|
||||
}: {
|
||||
status: JobRunStatus;
|
||||
basicStatus: RunBasicStatus;
|
||||
}) {
|
||||
switch (basicStatus) {
|
||||
case "COMPLETED":
|
||||
return (
|
||||
<Paragraph variant="small">There were no tasks for this run.</Paragraph>
|
||||
);
|
||||
case "FAILED":
|
||||
return <Paragraph variant="small">No tasks were run.</Paragraph>;
|
||||
case "WAITING":
|
||||
case "PENDING":
|
||||
case "RUNNING":
|
||||
return (
|
||||
<div>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Waiting for tasks…
|
||||
</Paragraph>
|
||||
<TaskCardSkeleton />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Paragraph variant="small">There were no tasks for this run.</Paragraph>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function RerunPopover({
|
||||
runId,
|
||||
runsPath,
|
||||
environmentType,
|
||||
status,
|
||||
}: {
|
||||
runId: string;
|
||||
runsPath: string;
|
||||
environmentType: RuntimeEnvironmentType;
|
||||
status: RunBasicStatus;
|
||||
}) {
|
||||
const lastSubmission = useActionData();
|
||||
|
||||
const [form, { successRedirect }] = useForm({
|
||||
id: "rerun",
|
||||
lastSubmission,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<Button variant="primary/small" shortcut={{ key: "R" }}>
|
||||
Rerun Job
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex w-80 flex-col gap-2 p-4" align="end">
|
||||
<Form
|
||||
method="post"
|
||||
action={`/resources/runs/${runId}/rerun`}
|
||||
{...form.props}
|
||||
>
|
||||
<input
|
||||
{...conform.input(successRedirect, { type: "hidden" })}
|
||||
defaultValue={runsPath}
|
||||
/>
|
||||
{environmentType === "PRODUCTION" && (
|
||||
<Callout variant="warning">
|
||||
This will rerun this Job in your Production environment.
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-start gap-4 divide-y divide-slate-600">
|
||||
<div>
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
name={conform.INTENT}
|
||||
value="start"
|
||||
fullWidth
|
||||
LeadingIcon={BoltIcon}
|
||||
>
|
||||
Run again
|
||||
</Button>
|
||||
|
||||
<Paragraph variant="extra-small" className="mt-2">
|
||||
Start a brand new job run with the same Trigger data as this
|
||||
one. This will re-do every task.
|
||||
</Paragraph>
|
||||
</div>
|
||||
{status === "FAILED" && (
|
||||
<div className="pt-4">
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
name={conform.INTENT}
|
||||
value="continue"
|
||||
fullWidth
|
||||
LeadingIcon={ForwardIcon}
|
||||
>
|
||||
Retry job run
|
||||
</Button>
|
||||
|
||||
<Paragraph variant="extra-small" className="mt-2">
|
||||
Continue running this job run from where it left off. This
|
||||
will skip any task that has already been completed.
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export function CancelRun({ runId }: { runId: string }) {
|
||||
const lastSubmission = useActionData();
|
||||
const location = useLocation();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [form, { redirectUrl }] = useForm({
|
||||
id: "cancel-run",
|
||||
lastSubmission,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: cancelSchema });
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading =
|
||||
navigation.state === "submitting" && navigation.formData !== undefined;
|
||||
|
||||
return (
|
||||
<Form
|
||||
method="post"
|
||||
action={`/resources/runs/${runId}/cancel`}
|
||||
{...form.props}
|
||||
>
|
||||
<input
|
||||
{...conform.input(redirectUrl, { type: "hidden" })}
|
||||
defaultValue={location.pathname}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
LeadingIcon={isLoading ? "spinner-white" : "stop"}
|
||||
leadingIconClassName="text-white"
|
||||
variant="danger/small"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Canceling" : "Cancel run"}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
+4
-1
@@ -124,7 +124,10 @@ export function TaskCard({
|
||||
)}
|
||||
{connection && (
|
||||
<RunPanelIconProperty
|
||||
icon={connection.integration.definitionId}
|
||||
icon={
|
||||
connection.integration.definition.icon ??
|
||||
connection.integration.definitionId
|
||||
}
|
||||
label="Connection"
|
||||
value={connection.integration.slug}
|
||||
/>
|
||||
@@ -0,0 +1,175 @@
|
||||
import { DetailedTask } from "~/presenters/TaskDetailsPresenter.server";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelDescription,
|
||||
RunPanelDivider,
|
||||
RunPanelHeader,
|
||||
RunPanelIconProperty,
|
||||
RunPanelIconSection,
|
||||
RunPanelIconTitle,
|
||||
RunPanelProperties,
|
||||
UpdatingDelay,
|
||||
UpdatingDuration,
|
||||
} from "./RunCard";
|
||||
import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Header3 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableCell,
|
||||
Table,
|
||||
} from "../primitives/Table";
|
||||
import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
|
||||
import { TaskStatusIcon } from "./TaskStatus";
|
||||
|
||||
export function TaskDetail({ task }: { task: DetailedTask }) {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
startedAt,
|
||||
completedAt,
|
||||
status,
|
||||
delayUntil,
|
||||
params,
|
||||
properties,
|
||||
output,
|
||||
style,
|
||||
attempts,
|
||||
} = task;
|
||||
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
<RunPanelHeader
|
||||
icon={
|
||||
<TaskStatusIcon
|
||||
status={status}
|
||||
minimal={true}
|
||||
className={cn("h-5 w-5")}
|
||||
/>
|
||||
}
|
||||
title={<RunPanelIconTitle icon={icon} title={name} />}
|
||||
accessory={
|
||||
<Paragraph variant="extra-small">
|
||||
<UpdatingDuration
|
||||
start={startedAt ?? undefined}
|
||||
end={completedAt ?? undefined}
|
||||
/>
|
||||
</Paragraph>
|
||||
}
|
||||
/>
|
||||
<RunPanelBody>
|
||||
<RunPanelIconSection>
|
||||
{startedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Started at"
|
||||
value={<DateTime date={startedAt} />}
|
||||
/>
|
||||
)}
|
||||
{completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={<DateTime date={completedAt} />}
|
||||
/>
|
||||
)}
|
||||
{delayUntil && !completedAt && (
|
||||
<>
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Continues at"
|
||||
value={<DateTime date={delayUntil} />}
|
||||
/>
|
||||
<UpdatingDelay delayUntil={delayUntil} />
|
||||
</>
|
||||
)}
|
||||
{delayUntil && completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="clock"
|
||||
label="Delay duration"
|
||||
value={formatDuration(startedAt, delayUntil, {
|
||||
style: "long",
|
||||
maxDecimalPoints: 0,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</RunPanelIconSection>
|
||||
<RunPanelDivider />
|
||||
{description && (
|
||||
<RunPanelDescription text={description} variant={style?.variant} />
|
||||
)}
|
||||
{properties.length > 0 && (
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Properties</Header3>
|
||||
<RunPanelProperties properties={properties} layout="horizontal" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attempts.length > 1 && (
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Retries</Header3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Attempt</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Date</TableHeaderCell>
|
||||
<TableHeaderCell>Error</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{attempts.map((attempt) => (
|
||||
<TableRow key={attempt.number}>
|
||||
<TableCell>{attempt.number}</TableCell>
|
||||
<TableCell>
|
||||
<TaskAttemptStatusLabel status={attempt.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DateTime
|
||||
date={
|
||||
attempt.status === "PENDING" && attempt.runAt
|
||||
? attempt.runAt
|
||||
: attempt.updatedAt
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{attempt.error}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Input</Header3>
|
||||
{params ? (
|
||||
<CodeBlock
|
||||
code={JSON.stringify(params, sensitiveDataReplacer, 2)}
|
||||
maxLines={35}
|
||||
/>
|
||||
) : (
|
||||
<Paragraph variant="small">No input</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Output</Header3>
|
||||
{output ? (
|
||||
<CodeBlock code={JSON.stringify(output, null, 2)} />
|
||||
) : (
|
||||
<Paragraph variant="small">No output</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
);
|
||||
}
|
||||
+9
@@ -3,6 +3,7 @@ import {
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
ClockIcon,
|
||||
NoSymbolIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
@@ -50,6 +51,12 @@ export function TaskStatusIcon({
|
||||
className={cn(taskStatusClassNameColor(status), className)}
|
||||
/>
|
||||
);
|
||||
case "CANCELED":
|
||||
return (
|
||||
<NoSymbolIcon
|
||||
className={cn(taskStatusClassNameColor(status), className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,5 +72,7 @@ function taskStatusClassNameColor(status: TaskStatus): string {
|
||||
return "text-blue-500";
|
||||
case "ERRORED":
|
||||
return "text-rose-500";
|
||||
case "CANCELED":
|
||||
return "text-slate-500";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { DetailedEvent } from "~/presenters/TriggerDetailsPresenter.server";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Header3 } from "../primitives/Headers";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelDivider,
|
||||
RunPanelHeader,
|
||||
RunPanelIconProperty,
|
||||
RunPanelIconSection,
|
||||
RunPanelProperties,
|
||||
} from "./RunCard";
|
||||
import { DisplayProperty } from "@trigger.dev/internal";
|
||||
|
||||
export function TriggerDetail({
|
||||
trigger,
|
||||
event,
|
||||
properties,
|
||||
}: {
|
||||
trigger: DetailedEvent;
|
||||
event: {
|
||||
title: string;
|
||||
icon: string;
|
||||
};
|
||||
properties: DisplayProperty[];
|
||||
}) {
|
||||
const { id, name, payload, timestamp, deliveredAt } = trigger;
|
||||
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
<RunPanelHeader icon={event.icon} title={event.title} />
|
||||
<RunPanelBody>
|
||||
<RunPanelIconSection>
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Created"
|
||||
value={<DateTime date={timestamp} />}
|
||||
/>
|
||||
{deliveredAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={<DateTime date={deliveredAt} />}
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconProperty icon="id" label="Event name" value={name} />
|
||||
</RunPanelIconSection>
|
||||
<RunPanelDivider />
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{properties.length > 0 && (
|
||||
<div className="mb-2 flex flex-col gap-4">
|
||||
<Header3>Properties</Header3>
|
||||
<RunPanelProperties properties={properties} layout="horizontal" />
|
||||
</div>
|
||||
)}
|
||||
<Header3>Payload</Header3>
|
||||
<CodeBlock code={JSON.stringify(payload, null, 2)} />
|
||||
</div>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,25 @@
|
||||
import type { JobRunStatus } from "@trigger.dev/database";
|
||||
import type { JobRunExecution, JobRunStatus } from "@trigger.dev/database";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
ExclamationTriangleIcon,
|
||||
StopIcon,
|
||||
WrenchIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import { HandRaisedIcon, NoSymbolIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export function hasFinished(status: JobRunStatus): boolean {
|
||||
return (
|
||||
status === "SUCCESS" ||
|
||||
status === "FAILURE" ||
|
||||
status === "ABORTED" ||
|
||||
status === "TIMED_OUT" ||
|
||||
status === "CANCELED"
|
||||
);
|
||||
}
|
||||
|
||||
export function RunStatus({ status }: { status: JobRunStatus }) {
|
||||
return (
|
||||
@@ -80,6 +92,12 @@ export function RunStatusIcon({
|
||||
return (
|
||||
<Spinner className={cn(runStatusClassNameColor(status), className)} />
|
||||
);
|
||||
case "CANCELED":
|
||||
return (
|
||||
<NoSymbolIcon
|
||||
className={cn(runStatusClassNameColor(status), className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +128,8 @@ export function runBasicStatus(status: JobRunStatus): RunBasicStatus {
|
||||
return "FAILED";
|
||||
case "PREPROCESSING":
|
||||
return "PENDING";
|
||||
case "CANCELED":
|
||||
return "FAILED";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +153,8 @@ export function runStatusTitle(status: JobRunStatus): string {
|
||||
return "Aborted";
|
||||
case "PREPROCESSING":
|
||||
return "Preprocessing";
|
||||
case "CANCELED":
|
||||
return "Canceled";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,5 +178,7 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
|
||||
return "text-rose-500";
|
||||
case "PREPROCESSING":
|
||||
return "text-blue-500";
|
||||
case "CANCELED":
|
||||
return "text-slate-500";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { RunList } from "~/presenters/RunListPresenter.server";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { runDashboardPath } from "~/utils/pathBuilder";
|
||||
import {
|
||||
JobForPath,
|
||||
OrgForPath,
|
||||
ProjectForPath,
|
||||
jobRunDashboardPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
@@ -21,22 +26,37 @@ import {
|
||||
TableRow,
|
||||
} from "../primitives/Table";
|
||||
import { RunStatus } from "./RunStatuses";
|
||||
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
|
||||
type RunTableItem = {
|
||||
id: string;
|
||||
number: number;
|
||||
environment: {
|
||||
type: RuntimeEnvironmentType;
|
||||
};
|
||||
status: JobRunStatus;
|
||||
startedAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
createdAt: Date | null;
|
||||
version: string;
|
||||
isTest: boolean;
|
||||
};
|
||||
|
||||
type RunsTableProps = {
|
||||
total: number;
|
||||
hasFilters: boolean;
|
||||
runs: RunTableItem[];
|
||||
isLoading?: boolean;
|
||||
runsParentPath: string;
|
||||
};
|
||||
|
||||
export function RunsTable({
|
||||
total,
|
||||
hasFilters,
|
||||
runs,
|
||||
isLoading = false,
|
||||
}: {
|
||||
total: number;
|
||||
hasFilters: boolean;
|
||||
runs: RunList["runs"];
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const job = useJob();
|
||||
|
||||
runsParentPath,
|
||||
}: RunsTableProps) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -65,7 +85,7 @@ export function RunsTable({
|
||||
</TableBlankRow>
|
||||
) : (
|
||||
runs.map((run) => {
|
||||
const path = runDashboardPath(organization, project, job, run);
|
||||
const path = `${runsParentPath}/${run.id}/trigger`;
|
||||
return (
|
||||
<TableRow key={run.id}>
|
||||
<TableCell to={path}>#{run.number}</TableCell>
|
||||
|
||||
@@ -11,9 +11,10 @@ import { MainCenteredContainer } from "../layout/AppLayout";
|
||||
import { FormTitle } from "../primitives/FormTitle";
|
||||
import { Form } from "@remix-run/react";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { Paragraph, TextLink } from "../primitives/Paragraph";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { LogoIcon } from "../LogoIcon";
|
||||
import { Label } from "../primitives/Label";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
|
||||
const meta: Meta<typeof Forms> = {
|
||||
title: "Primitives/Forms",
|
||||
@@ -175,6 +176,33 @@ function SearchForm() {
|
||||
fullWidth={false}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label variant="small">This is a small label</Label>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
required={true}
|
||||
icon="search"
|
||||
shortcut="⌘K"
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label variant="medium">This is a medium label</Label>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
required={true}
|
||||
icon="search"
|
||||
shortcut="⌘K"
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label variant="large">This is a large label</Label>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
required={true}
|
||||
icon="search"
|
||||
shortcut="⌘K"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import { Header1, Header2, Header3 } from "../primitives/Headers";
|
||||
import { Paragraph, TextLink } from "../primitives/Paragraph";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
|
||||
const meta: Meta<typeof Typography> = {
|
||||
title: "Primitives/Typography",
|
||||
@@ -43,7 +44,7 @@ function Typography({ header1, header2, header3, paragraph }: TypographyProps) {
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<Header1>{header1}</Header1>
|
||||
<Header1 variant="dimmed">{header1}</Header1>
|
||||
<Header1 textColor="dimmed">{header1}</Header1>
|
||||
<Header2>{header2}</Header2>
|
||||
<Header3>{header3}</Header3>
|
||||
<Paragraph>{paragraph}</Paragraph>
|
||||
|
||||
@@ -7,16 +7,12 @@ const EnvironmentSchema = z.object({
|
||||
z.literal("production"),
|
||||
z.literal("test"),
|
||||
]),
|
||||
REMIX_APP_PORT: z.string().optional(),
|
||||
DATABASE_URL: z.string(),
|
||||
LOGIN_ORIGIN: z
|
||||
.string()
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
.default(process.env.FC_URL ?? "https://app.trigger.dev"),
|
||||
APP_ORIGIN: z
|
||||
.string()
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
.default(process.env.FC_URL ?? "https://app.trigger.dev"),
|
||||
SESSION_SECRET: z.string(),
|
||||
MAGIC_LINK_SECRET: z.string(),
|
||||
REMIX_APP_PORT: z.string().optional(),
|
||||
LOGIN_ORIGIN: z.string().default("http://localhost:3030"),
|
||||
APP_ORIGIN: z.string().default("http://localhost:3030"),
|
||||
APP_ENV: z
|
||||
.union([
|
||||
z.literal("development"),
|
||||
@@ -28,14 +24,13 @@ const EnvironmentSchema = z.object({
|
||||
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(),
|
||||
FROM_EMAIL: z.string(),
|
||||
REPLY_TO_EMAIL: z.string(),
|
||||
RESEND_API_KEY: z.string(),
|
||||
SESSION_SECRET: z.string(),
|
||||
FROM_EMAIL: z.string().optional(),
|
||||
REPLY_TO_EMAIL: z.string().optional(),
|
||||
RESEND_API_KEY: z.string().optional(),
|
||||
PLAIN_API_KEY: z.string().optional(),
|
||||
RUNTIME_PLATFORM: z.enum(["docker-compose", "ecs", "local"]).default("local"),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import { DEV_ENVIRONMENT, LIVE_ENVIRONMENT } from "~/consts";
|
||||
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { useMatchesData } from "~/utils";
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { MatchedProject, useOptionalProject } from "./useProject";
|
||||
|
||||
export type ProjectJobEnvironment = MatchedProject["environments"][number];
|
||||
|
||||
export function useEnvironments() {
|
||||
const project = useOptionalProject();
|
||||
|
||||
if (!project) {
|
||||
return undefined;
|
||||
}
|
||||
export function useEnvironments(matches?: RouteMatch[]) {
|
||||
const project = useOptionalProject(matches);
|
||||
if (!project) return;
|
||||
|
||||
return project.environments;
|
||||
}
|
||||
|
||||
export function useDevEnvironment() {
|
||||
const environments = useEnvironments();
|
||||
export function useDevEnvironment(matches?: RouteMatch[]) {
|
||||
const environments = useEnvironments(matches);
|
||||
if (!environments) return;
|
||||
|
||||
return environments.find((environment) => environment.type === "DEVELOPMENT");
|
||||
}
|
||||
|
||||
export function useProdEnvironment() {
|
||||
const environments = useEnvironments();
|
||||
export function useProdEnvironment(matches?: RouteMatch[]) {
|
||||
const environments = useEnvironments(matches);
|
||||
if (!environments) return;
|
||||
|
||||
return environments.find((environment) => environment.type === "PRODUCTION");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ProjectJob } from "./useProject";
|
||||
import { ProjectJob } from "./useJobs";
|
||||
import { useTextFilter } from "./useTextFilter";
|
||||
|
||||
export function useFilterJobs(jobs: ProjectJob[]) {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import {
|
||||
UseDataFunctionReturn,
|
||||
useTypedRouteLoaderData,
|
||||
} from "remix-typedjson";
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type MatchedClient = UseDataFunctionReturn<typeof loader>["client"];
|
||||
|
||||
export function useOptionalIntegrationClient() {
|
||||
const routeMatch = useTypedRouteLoaderData<typeof loader>(
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam"
|
||||
);
|
||||
export function useOptionalIntegrationClient(matches?: RouteMatch[]) {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam",
|
||||
matches,
|
||||
});
|
||||
|
||||
return routeMatch?.client;
|
||||
}
|
||||
|
||||
export function useIntegrationClient() {
|
||||
const integration = useOptionalIntegrationClient();
|
||||
export function useIntegrationClient(matches?: RouteMatch[]) {
|
||||
const integration = useOptionalIntegrationClient(matches);
|
||||
invariant(integration, "Integration must be defined");
|
||||
return integration;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { RouteMatch, useMatches } from "@remix-run/react";
|
||||
|
||||
export function useIsProjectChildPage() {
|
||||
const matchesData = useMatches();
|
||||
export function useIsProjectChildPage(matches?: RouteMatch[]) {
|
||||
if (!matches) {
|
||||
matches = useMatches();
|
||||
}
|
||||
|
||||
return matchesData.some((matchData) => {
|
||||
return matches.some((matchData) => {
|
||||
return matchData.id.startsWith(
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam"
|
||||
);
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import {
|
||||
UseDataFunctionReturn,
|
||||
useTypedRouteLoaderData,
|
||||
} from "remix-typedjson";
|
||||
import { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
|
||||
import { useOptionalProject } from "./useProject";
|
||||
import { useChanged } from "./useChanged";
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
|
||||
|
||||
export function useOptionalJob() {
|
||||
const project = useOptionalProject();
|
||||
const routeMatch = useTypedRouteLoaderData<typeof loader>(
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam"
|
||||
);
|
||||
export const jobMatchId =
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
|
||||
export function useOptionalJob(matches?: RouteMatch[]) {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: jobMatchId,
|
||||
matches,
|
||||
});
|
||||
|
||||
if (!project || !routeMatch || !routeMatch.job) {
|
||||
if (!routeMatch || !routeMatch.job) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (routeMatch.job == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
//get the job from the list on the project
|
||||
return project.jobs.find((j) => j.id === routeMatch.job.id);
|
||||
return routeMatch.projectJobs.find((j) => j.id === routeMatch.job.id);
|
||||
}
|
||||
|
||||
export function useJob() {
|
||||
const job = useOptionalJob();
|
||||
export function useJob(matches?: RouteMatch[]) {
|
||||
const job = useOptionalJob(matches);
|
||||
invariant(job, "Job must be defined");
|
||||
return job;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type ProjectJob = UseDataFunctionReturn<
|
||||
typeof loader
|
||||
>["projectJobs"][number];
|
||||
|
||||
export const jobsMatchId =
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
|
||||
export function useOptionalJobs(matches?: RouteMatch[]) {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: jobsMatchId,
|
||||
matches,
|
||||
});
|
||||
|
||||
return routeMatch?.projectJobs;
|
||||
}
|
||||
|
||||
export function useJobs(matches?: RouteMatch[]) {
|
||||
const jobs = useOptionalJobs(matches);
|
||||
invariant(jobs, "Jobs must be defined");
|
||||
return jobs;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user