Compare commits
599 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfb00e84e9 | |||
| b5a64545bb | |||
| 3cf3eaff48 | |||
| 4ca758a3de | |||
| da81537009 | |||
| 7c1b13ba3b | |||
| 4b654e5b3b | |||
| 726a50ace7 | |||
| 9deffb67c4 | |||
| ff04bf44ee | |||
| e740297829 | |||
| a31705e198 | |||
| f0bdf53364 | |||
| 9638499163 | |||
| 9104c252b7 | |||
| 72cf345602 | |||
| d5b8f8299d | |||
| dd22b88d25 | |||
| 62b9c5879c | |||
| 1b0973fbc1 | |||
| fc78854ed4 | |||
| ed15bb0618 | |||
| ced9034428 | |||
| 7ec329d5d6 | |||
| 81180999de | |||
| feaa79ecf3 | |||
| d34dc0847c | |||
| 1b7c7520b4 | |||
| 34d77e830c | |||
| 87c302bf26 | |||
| d462c901f5 | |||
| 55c3d79cc8 | |||
| ce901c92e4 | |||
| 0fd409c6dc | |||
| f4e7379768 | |||
| ee99191fb4 | |||
| 18124ae32a | |||
| de3373b8f9 | |||
| bdb4e6e2bd | |||
| 0a790de2c1 | |||
| ffb55bfde6 | |||
| c0fbe56f79 | |||
| 342ab0d471 | |||
| aa9fe7d408 | |||
| f499c10f09 | |||
| 45ed7cdf38 | |||
| fa271aaed2 | |||
| 8883476dcb | |||
| c1755ff85b | |||
| 6d04f6c62a | |||
| 53363711bb | |||
| 442ee45e97 | |||
| 8405b337f9 | |||
| facae926ce | |||
| 9544624e00 | |||
| a489fd465f | |||
| 9ff4c0dbd5 | |||
| 36ec16babe | |||
| 92233f2e90 | |||
| efd590976c | |||
| 463cbed7df | |||
| 9138976dad | |||
| ace4d5f6ce | |||
| b09ecb0b06 | |||
| 10fef0f667 | |||
| 3e5945fb9e | |||
| fdf1b2148c | |||
| 7e4d72f959 | |||
| 3e203ea967 | |||
| af56598a58 | |||
| 1fef1fa5f8 | |||
| bd2f68a51d | |||
| 64477f6b3a | |||
| 8b09dfd697 | |||
| f330673143 | |||
| 9351c05126 | |||
| af722e54d8 | |||
| 1eb89844f4 | |||
| 004423aea3 | |||
| 369c6245ab | |||
| b8ae937966 | |||
| 3b510a4f3d | |||
| b3e609d53d | |||
| 2f07a792e9 | |||
| 078bf2bc9c | |||
| a7c8f0b90e | |||
| 495acf4319 | |||
| dce86119d4 | |||
| 2c70b7a360 | |||
| c518185947 | |||
| b314178de2 | |||
| e0a2cec978 | |||
| d3608be1a0 | |||
| 4c79129242 | |||
| 8ac83f1d59 | |||
| 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 | |||
| 79eab3d62f | |||
| 6527723a0e | |||
| 3d6652f132 | |||
| da5d9779d4 | |||
| 865399e753 | |||
| 960ea6c8dd | |||
| 39ab48f0cb | |||
| 7341b4cf61 | |||
| 28c5acc6af | |||
| 4366eefc66 | |||
| cb95e761c1 | |||
| 7136748463 | |||
| b0cacfd75c | |||
| 81f03dac79 | |||
| e37d1eadcd | |||
| 23fb310374 | |||
| 797a1732d2 | |||
| c405a417e9 | |||
| 808deb1d77 | |||
| 6ac1473ec3 | |||
| 9c183abf4b | |||
| 33c33faae9 | |||
| 4cccbc2039 | |||
| cc061dedf8 | |||
| 402ce2210f | |||
| 329c5c4070 | |||
| 878aeb7a8c | |||
| 3ee396d788 | |||
| 090037602c | |||
| bee77051e7 | |||
| be6741a4a8 | |||
| 3e9b440322 | |||
| dfea5bc77c | |||
| 1eef30e298 | |||
| e5b9a9de3f | |||
| dfedd3a6e8 | |||
| a19845b4ca | |||
| 094f6f5a7a | |||
| fa60edce2b | |||
| ba44652457 | |||
| 931be39912 | |||
| 5048a27acb | |||
| 71fc615b31 | |||
| 20ed4e22ef | |||
| fc2d337a39 | |||
| 31d96f3862 | |||
| 7b43853ec6 | |||
| 40fa913272 | |||
| cdd5482aad | |||
| 4a3fa20ee7 | |||
| d49ed4cd47 | |||
| db3cf77f5b | |||
| c4a08961d6 | |||
| 5ba1643d46 | |||
| d189f425e8 | |||
| 0fe6fb83ac | |||
| f15ae41f48 | |||
| 46a07fc72e | |||
| 83af317739 | |||
| f4db1b2735 | |||
| 6d4922f419 | |||
| 6af098ee8a | |||
| 1dcb2ebcac | |||
| f65cb463cc | |||
| 2b4ce97f8a | |||
| 25abf319ab | |||
| 77807ab42a | |||
| f21990b14f | |||
| 1873d10e95 | |||
| 32ff3f3136 | |||
| 354b0aefb0 | |||
| aaa70a9aab | |||
| 3b90739087 | |||
| acaae993a5 | |||
| a74f0322e2 | |||
| 1a0286f6b0 | |||
| 64182483e2 | |||
| 87a25fba62 | |||
| 12803536fa | |||
| f160b34b34 | |||
| cca7da9da2 | |||
| 01cf5f3bdc | |||
| 33a928a9ea | |||
| c83443a41a | |||
| 374007bffd | |||
| 092382f319 | |||
| 79a0532e1d | |||
| 56c7c8b1ee | |||
| 722fe7b71e | |||
| 759793b2e3 | |||
| 99c6cd0371 | |||
| 9fbc5ab8de | |||
| 75ac2c9961 | |||
| 87330dd863 | |||
| 38fe9302cf | |||
| 33a455c593 | |||
| 37db0ac58d | |||
| 2cbf50b1cc | |||
| e26923eb19 | |||
| 3e63bcfb11 | |||
| 1e1c18c4a1 | |||
| 953e7fc918 | |||
| 8e147dbe0c | |||
| 86dbd5d1dc | |||
| e85ecfacba | |||
| f210532870 | |||
| 6c9ca686cb | |||
| 35b556d4a3 | |||
| c83339404a | |||
| 4172def1a3 | |||
| c99481ea8d | |||
| a11ddf6564 | |||
| 917a70fbe9 | |||
| 486d6818ba | |||
| 807b9d4c45 | |||
| 0013e78b1a | |||
| 9dd46c83fd | |||
| 1eb6bb8aa9 | |||
| 6c86946614 | |||
| 612b016a15 | |||
| be8beacaec | |||
| 4d316c5524 | |||
| 7fee5d6a87 | |||
| 0dd1aa5a5b | |||
| ed65ba4a73 | |||
| d39020370c | |||
| c18ba11819 | |||
| 5fbd7226eb | |||
| c9e27fd586 | |||
| 51b80eb09b | |||
| f409e9f329 | |||
| 304e6aaad5 | |||
| 3c5d02043e | |||
| 63980f63d4 | |||
| facd2b2131 | |||
| 25452588cc | |||
| 195d8c42fc | |||
| fe04b9d32d | |||
| d03b3aeb5f | |||
| 2ee33e70f7 | |||
| e9209629f7 | |||
| fa1e6bf5e6 | |||
| 93c3b10bf7 | |||
| 56cb2e3b07 | |||
| eabfb2457a | |||
| 317ab442ee | |||
| 2b24b36da6 | |||
| 161d59d01d | |||
| 821b75ed26 | |||
| 1f901e68d3 | |||
| 46d94a6a8f | |||
| 44c2146505 | |||
| ec2f1f5931 | |||
| e86c59f4ec | |||
| 6df6813a17 | |||
| f73679e2a6 | |||
| 68b866921a | |||
| 35364d615a | |||
| f64dd2a99c | |||
| a309a75a4d | |||
| c2e2f7cad9 | |||
| f69b92b153 | |||
| f8760fdd6b | |||
| caa4daabe2 | |||
| 67f685af26 | |||
| b2a8b5653f | |||
| 9232f019e1 | |||
| ac371664af | |||
| e4cca81124 | |||
| d76d2b86d2 | |||
| fcd2d6eea2 | |||
| 067064d19c | |||
| 52cdfb98a4 | |||
| 41261afcc4 | |||
| 77f71fdeb7 | |||
| ce0032e7e5 | |||
| c97ca350ad | |||
| 90abcb2cf7 | |||
| 8626172faa | |||
| 44ef97c656 | |||
| 7661fe5309 | |||
| beb02c871d | |||
| 8a47320272 | |||
| 114c8990a3 | |||
| 7e2d48ac6a | |||
| dbb6667889 | |||
| 29a02131d2 | |||
| 5d887f1cfb | |||
| 18f0e73ed4 | |||
| c4c1425c9b | |||
| 00bb3934ad | |||
| 324096e14e | |||
| 9cad2bb308 | |||
| eec537135a | |||
| 53e42d3350 | |||
| e74122977a | |||
| f32c3ebe2c | |||
| d07ac4fef0 | |||
| 183761b01f | |||
| 361ef70ce8 | |||
| 2bac0a4e5b | |||
| b89a68bb9c | |||
| 496dadebbd |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Adding support for trigger source in the run context, and make sure dynamic trigger runs are preprocessed so they have a chance of populating run properties
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
A number of improvements to the initialization experience:
|
||||
|
||||
- Detect middleware usage and warn about how it could conflict with Trigger.dev and with a link to the docs
|
||||
- Lookup package versions and use the latest versions
|
||||
- Better error messages when registering an endpoint doesn't work
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/github": patch
|
||||
---
|
||||
|
||||
Fixed the broken export issue with two of the new tasks
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fix for Next.js projects using the src dir
|
||||
@@ -2,13 +2,10 @@
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"fixed": [["@trigger.dev/*"]],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"webapp",
|
||||
"emails"
|
||||
]
|
||||
}
|
||||
"ignore": ["webapp", "emails", "@trigger.dev/database"]
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/integration-kit": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/slack": patch
|
||||
"@trigger.dev/nextjs": patch
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Creating the init CLI package
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": major
|
||||
"@trigger.dev/github": major
|
||||
"@trigger.dev/slack": major
|
||||
"@trigger.dev/nextjs": major
|
||||
---
|
||||
|
||||
Preparing packages for V2
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Remove the localhost default for endpoint URL
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fixed the version of the @trigger.dev/nextjs package
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Endpoint registration and indexing now is only initiated outside of clients
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Add support for task errors and task retrying
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Improved the CLI prompts
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Fixed the eventTrigger name
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Adding more granular error messages around unauthorized requests
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/slack": patch
|
||||
---
|
||||
|
||||
Added support for backgroundFetch
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "next",
|
||||
"initialVersions": {
|
||||
"webapp": "1.0.0",
|
||||
"@trigger.dev/github": "0.2.0",
|
||||
"@trigger.dev/slack": "0.2.0",
|
||||
"create-trigger": "0.2.0",
|
||||
"emails": "1.0.0",
|
||||
"@trigger.dev/internal": "0.0.1",
|
||||
"@trigger.dev/nextjs": "0.1.0",
|
||||
"@trigger.dev/sdk": "1.0.0",
|
||||
"@trigger.dev/openai": "1.0.0-next.0",
|
||||
"@trigger.dev/database": "0.0.0",
|
||||
"@trigger.dev/init": "0.2.0",
|
||||
"@trigger.dev/integration-kit": "2.0.0-next.1",
|
||||
"@trigger.dev/resend": "1.0.0-next.1"
|
||||
},
|
||||
"changesets": [
|
||||
"bright-horses-bathe",
|
||||
"chatty-cars-agree",
|
||||
"chilly-meals-warn",
|
||||
"clever-bats-learn",
|
||||
"curly-hornets-sort",
|
||||
"dirty-needles-kneel",
|
||||
"eight-buckets-agree",
|
||||
"eighty-queens-prove",
|
||||
"four-ligers-perform",
|
||||
"great-meals-listen",
|
||||
"heavy-mails-explain",
|
||||
"itchy-rockets-provide",
|
||||
"metal-donkeys-relate",
|
||||
"modern-emus-suffer",
|
||||
"quick-stingrays-own",
|
||||
"strong-cars-visit",
|
||||
"stupid-files-cough",
|
||||
"sweet-snails-refuse",
|
||||
"tame-flowers-invite",
|
||||
"tough-walls-share",
|
||||
"wild-rats-mix"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fixed the help defaults
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fixed an issue that cause environment variables to be incorrectly added to .env.local
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Loosen the peer dep requirement on next
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fixed CLI showing undefined when not specifying a trigger-url flag
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/resend": patch
|
||||
---
|
||||
|
||||
Adding the resend integration package
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Make sure nextjs app routes have the correct types
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Update the devDependency on nextjs to be more permissive
|
||||
+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
|
||||
|
||||
+11
-14
@@ -1,13 +1,7 @@
|
||||
# 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
|
||||
ENCRYPTION_KEY=ae13021afef0819c3a307ad487071c06 # Must be a random 16 byte hex string. You can generate an encryption key by running `openssl rand -hex 16` in your terminal
|
||||
LOGIN_ORIGIN=http://localhost:3030
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public
|
||||
REMIX_APP_PORT=3030
|
||||
@@ -16,17 +10,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=
|
||||
|
||||
@@ -5,7 +5,23 @@ on:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*"
|
||||
- "v.docker.*"
|
||||
paths:
|
||||
- ".github/workflows/publish.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- "!packages/**/*.eslintrc"
|
||||
- "apps/**"
|
||||
- "!apps/**/*.md"
|
||||
- "!apps/**/*.eslintrc"
|
||||
- "integrations/**"
|
||||
- "!integrations/**/*.md"
|
||||
- "!integrations/**/*.eslintrc"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
- "docker/Dockerfile"
|
||||
- "docker/scripts/**"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -63,6 +79,10 @@ jobs:
|
||||
run: |
|
||||
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
if [[ $IMAGE_TAG == v.docker.* ]]; then
|
||||
ORIGINAL_VERSION="${IMAGE_TAG#v.docker.}"
|
||||
IMAGE_TAG="v${ORIGINAL_VERSION}"
|
||||
fi
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
elif [[ $GITHUB_REF == refs/heads/* ]]; then
|
||||
IMAGE_TAG="${GITHUB_REF#refs/heads/}"
|
||||
@@ -71,8 +91,7 @@ jobs:
|
||||
echo "Invalid reference: ${GITHUB_REF}"
|
||||
exit 1
|
||||
fi
|
||||
echo ::set-output name=version::${IMAGE_TAG}
|
||||
|
||||
echo "::set-output name=version::${IMAGE_TAG}"
|
||||
- name: 🔢 Get the commit hash
|
||||
id: get_commit
|
||||
run: |
|
||||
@@ -106,48 +125,3 @@ jobs:
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: trigger.dev
|
||||
|
||||
- name: 🌐 Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::067435167850:role/GitHubAction-AssumeRoleWithAction
|
||||
aws-region: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: 🌐 Login to ECR
|
||||
id: ecr-login
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
|
||||
- name: 🌐 Push to Amazon ECR
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
- name: 🌐 Push 'latest' to Amazon ECR
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
|
||||
docker push $REGISTRY/$REPOSITORY:latest
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
|
||||
- name: 🌐 Push commit hash to Amazon ECR
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}-${{ steps.get_commit.outputs.sha_short }}
|
||||
|
||||
- name: 🚀 Trigger Test Deploy
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push' && github.repository == 'triggerdotdev/trigger.dev'
|
||||
env:
|
||||
DEPLOY_TEST_HOOK: ${{ secrets.DEPLOY_TEST_HOOK }}
|
||||
run: |
|
||||
curl -X POST $DEPLOY_TEST_HOOK
|
||||
|
||||
@@ -4,6 +4,16 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/release.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- "changesets/**"
|
||||
- "integrations/**"
|
||||
- "!integrations/**/*.md"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"workspaceId":"63e5e42daf9a537ba8d9503c"}
|
||||
{ "workspaceId": "63e5e42daf9a537ba8d9503c" }
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.env
|
||||
.env.local
|
||||
pnpm-lock.yaml
|
||||
tailwind.css
|
||||
.babelrc.json
|
||||
**/.react-email/
|
||||
**/storybook-static/
|
||||
**/.changeset/
|
||||
**/build/
|
||||
**/dist/
|
||||
Vendored
+1
-8
@@ -5,19 +5,12 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "pnpm run idev --filter webapp",
|
||||
"command": "pnpm run dev --filter webapp",
|
||||
"name": "Run webapp",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev --filter integrations",
|
||||
"name": "Run integrations",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
|
||||
Vendored
-19
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "trigger.dev",
|
||||
"path": "../"
|
||||
},
|
||||
{
|
||||
"name": "Webapp",
|
||||
"path": "../apps/webapp"
|
||||
},
|
||||
{
|
||||
"name": "Integrations",
|
||||
"path": "../apps/integrations"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"vitest.commandLine": "pnpm exec vitest"
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
# 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 generate a new value for `ENCRYPTION_KEY`:
|
||||
|
||||
`ENCRYPTION_KEY` is used to two-way encrypt OAuth access tokens and so you'll probably want to actually generate a unique value, and it must be a random 16 byte hex string. You can generate one with the following command:
|
||||
|
||||
```sh
|
||||
openssl rand -hex 16
|
||||
```
|
||||
|
||||
Feel free to update `SESSION_SECRET` and `MAGIC_LINK_SECRET` as well using the same method.
|
||||
|
||||
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:
|
||||
|
||||
```sh
|
||||
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.
|
||||
```
|
||||
|
||||
Paste the magic link shown in your terminal into your browser to login.
|
||||
|
||||
## Adding and running migrations
|
||||
|
||||
1. Modify packages/database/prisma/schema.prisma file
|
||||
2. Change directory to the packages/database folder
|
||||
```sh
|
||||
cd packages/database
|
||||
```
|
||||
3. Generate the Prisma client
|
||||
|
||||
```sh
|
||||
pnpm run generate
|
||||
```
|
||||
|
||||
The above updates the prisma client generated into node_modules/.prisma/client folder. This helps with typing of relevant prisma models. It ensures typescript
|
||||
recognizes fields added or removed from a model and type-checks appropriately.
|
||||
|
||||
4. Create and apply the migrations
|
||||
|
||||
```
|
||||
pnpm run db:migrate:dev
|
||||
```
|
||||
|
||||
This creates a migration file and executes the migrations against your database and applies changes to the database schema(s)
|
||||
|
||||
5. Commit generated migrations as well as changes to the schema.prisma file
|
||||
6. If you're using VSCode you may need to restart the Typescript server in the webapp to get updated type inference. Open a TypeScript file, then open the Command Palette (View > Command Palette) and run `TypeScript: Restart TS server`.
|
||||
|
||||
## Testing CLI changes
|
||||
|
||||
To test CLI changes, follow the steps below:
|
||||
|
||||
1. Build the CLI and watch for changes
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": { "@trigger.dev/cli": "workspace:*" }
|
||||
}
|
||||
```
|
||||
|
||||
5. Open a new terminal window, navigate into the example, and initialize the CLI:
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```env
|
||||
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:
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```sh
|
||||
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`
|
||||
```sh
|
||||
lsof -i :3030
|
||||
```
|
||||
2. Kill the process
|
||||
```sh
|
||||
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,13 +7,15 @@
|
||||
|
||||
### 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)
|
||||
|
||||
</div>
|
||||
|
||||
# About Trigger.dev
|
||||
|
||||
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
|
||||
|
||||
### Long running Jobs on serverless
|
||||
@@ -60,46 +62,4 @@ 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/)
|
||||
|
||||
### 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
|
||||
```
|
||||
To setup and develop locally or contribute to the open source project, follow our [developement guide](./CONTRIBUTING.md).
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"@remix-run/eslint-config",
|
||||
"@remix-run/eslint-config/node",
|
||||
"prettier"
|
||||
],
|
||||
"extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node", "prettier"],
|
||||
"rules": {
|
||||
"@typescript-eslint/strict-boolean-expressions": [
|
||||
"error",
|
||||
|
||||
@@ -15,4 +15,6 @@ node_modules
|
||||
build-storybook.log
|
||||
.out
|
||||
.storybook-out
|
||||
storybook-static
|
||||
storybook-static
|
||||
|
||||
/prisma/seed.js
|
||||
@@ -20,10 +20,7 @@ const config: StorybookConfig = {
|
||||
},
|
||||
};
|
||||
},
|
||||
stories: [
|
||||
"../app/**/stories/*.mdx",
|
||||
"../app/**/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
stories: ["../app/**/stories/*.mdx", "../app/**/stories/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
|
||||
@@ -35,9 +35,7 @@ const preview: Preview = {
|
||||
|
||||
return (
|
||||
<OperatingSystemContextProvider platform="mac">
|
||||
<LocaleContextProvider
|
||||
locales={window.navigator.languages as string[]}
|
||||
>
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixStub initialEntries={["/"]} />
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
export function DynamicTriggerIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<svg viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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,11 +1,6 @@
|
||||
export function WebhookIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 190 180"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg className={className} viewBox="0 0 190 180" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
import { MainCenteredContainer } from "./layout/AppLayout";
|
||||
import { Header1, Header2 } from "./primitives/Headers";
|
||||
import { NamedIcon, NamedIconInBox } from "./primitives/NamedIcon";
|
||||
import { Header2 } from "./primitives/Headers";
|
||||
import { NamedIconInBox } from "./primitives/NamedIcon";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
|
||||
type ComingSoonProps = {
|
||||
@@ -16,16 +16,13 @@ export function ComingSoon({ title, description, icon }: ComingSoonProps) {
|
||||
<div className="flex flex-col justify-center rounded border border-slate-800 bg-slate-850">
|
||||
<div className="flex items-center gap-2 border-b border-slate-750 px-4 py-4">
|
||||
{typeof icon === "string" ? (
|
||||
<NamedIconInBox
|
||||
name={icon}
|
||||
className={"h-10 w-10 bg-midnight-800"}
|
||||
/>
|
||||
<NamedIconInBox name={icon} className={"h-10 w-10 bg-midnight-800"} />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
<div className="mt-0.5 flex flex-col">
|
||||
<div className="mt-0.5 flex flex-col gap-y-1">
|
||||
<Paragraph variant="extra-extra-small/caps">Coming soon</Paragraph>
|
||||
<Header1 className="-mt-0.5">{title}</Header1>
|
||||
<Header2 className="-mt-0.5">{title}</Header2>
|
||||
</div>
|
||||
</div>
|
||||
<Paragraph variant="small" className="p-4">
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
|
||||
import { LinkButton } from "./primitives/Buttons";
|
||||
import { Header1, Header3 } from "./primitives/Headers";
|
||||
import { friendlyErrorDisplay } from "~/utils/httpErrors";
|
||||
|
||||
type ErrorDisplayOptions = {
|
||||
button?: {
|
||||
title: string;
|
||||
to: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function RouteErrorDisplay(options?: ErrorDisplayOptions) {
|
||||
const error = useRouteError();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRouteErrorResponse(error) ? (
|
||||
<ErrorDisplay
|
||||
title={friendlyErrorDisplay(error.status, error.statusText).title}
|
||||
message={
|
||||
error.data.message ?? friendlyErrorDisplay(error.status, error.statusText).message
|
||||
}
|
||||
{...options}
|
||||
/>
|
||||
) : error instanceof Error ? (
|
||||
<ErrorDisplay title={error.name} message={error.message} {...options} />
|
||||
) : (
|
||||
<ErrorDisplay title="Oops" message={JSON.stringify(error)} {...options} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type DisplayOptionsProps = {
|
||||
title: string;
|
||||
message?: string;
|
||||
} & ErrorDisplayOptions;
|
||||
|
||||
export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Header1 className="mb-4 border-b border-slate-800 pb-4">{title}</Header1>
|
||||
{message && <Header3>{message}</Header3>}
|
||||
<LinkButton to={button ? button.to : "/"} variant="primary/medium" className="mt-8">
|
||||
{button ? button.title : "Home"}
|
||||
</LinkButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
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 { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
|
||||
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";
|
||||
@@ -24,16 +18,15 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./primitives/Select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTrigger,
|
||||
} from "./primitives/Sheet";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } 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,31 +51,21 @@ 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>
|
||||
<input
|
||||
value={location.pathname}
|
||||
{...conform.input(path, { type: "hidden" })}
|
||||
/>
|
||||
<input value={location.pathname} {...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>
|
||||
@@ -95,9 +78,7 @@ export function Feedback() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<FormError id={feedbackType.errorId}>
|
||||
{feedbackType.error}
|
||||
</FormError>
|
||||
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label>Message</Label>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { H, HighlightOptions } from "highlight.run";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface Props extends HighlightOptions {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export function HighlightInit({ projectId, ...highlightOptions }: Props) {
|
||||
useEffect(() => {
|
||||
projectId && H.init(projectId, highlightOptions);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -2,17 +2,12 @@ import { Form } from "@remix-run/react";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
|
||||
export function ImpersonationBanner({
|
||||
impersonationId,
|
||||
}: {
|
||||
impersonationId: string;
|
||||
}) {
|
||||
export function ImpersonationBanner({ impersonationId }: { impersonationId: string }) {
|
||||
return (
|
||||
<div className="grid h-8 grid-cols-3 items-center bg-gradient-to-r from-amber-500 via-amber-400 to-amber-500 px-4 text-center">
|
||||
<span></span>
|
||||
<Paragraph variant="small">
|
||||
You are impersonating{" "}
|
||||
<span className="font-semibold">{impersonationId}</span>
|
||||
You are impersonating <span className="font-semibold">{impersonationId}</span>
|
||||
</Paragraph>
|
||||
<Form
|
||||
action="/resources/impersonation"
|
||||
|
||||
@@ -10,9 +10,7 @@ export function NoMobileOverlay() {
|
||||
<div className="absolute mx-8 flex flex-col items-center gap-6 rounded-lg bg-slate-850 px-8 py-10 text-center">
|
||||
<XMarkIcon className="absolute top-14 h-8 w-8 text-rose-600" />
|
||||
<DevicePhoneMobileIcon className="h-16 w-16 text-slate-500" />
|
||||
<Paragraph>
|
||||
Trigger.dev is currently only available on desktop.
|
||||
</Paragraph>
|
||||
<Paragraph>Trigger.dev is currently only available on desktop.</Paragraph>
|
||||
<LinkButton to="https://trigger.dev" variant="primary/medium">
|
||||
Back Home
|
||||
</LinkButton>
|
||||
|
||||
@@ -4,13 +4,7 @@ import { cn } from "~/utils/cn";
|
||||
|
||||
export function UserProfilePhoto({ className }: { className?: string }) {
|
||||
const user = useOptionalUser();
|
||||
return (
|
||||
<UserAvatar
|
||||
avatarUrl={user?.avatarUrl}
|
||||
name={user?.name}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
return <UserAvatar avatarUrl={user?.avatarUrl} name={user?.name} className={className} />;
|
||||
}
|
||||
|
||||
export function UserAvatar({
|
||||
@@ -23,9 +17,9 @@ export function UserAvatar({
|
||||
className?: string;
|
||||
}) {
|
||||
return avatarUrl ? (
|
||||
<div>
|
||||
<div className={cn("grid aspect-square place-items-center", className)}>
|
||||
<img
|
||||
className={cn("aspect-square rounded-full p-[7%]", className)}
|
||||
className={cn("aspect-square rounded-full p-[7%]")}
|
||||
src={avatarUrl}
|
||||
alt={name ?? "User"}
|
||||
/>
|
||||
|
||||
@@ -2,16 +2,8 @@ import type { Language, PrismTheme } from "prism-react-renderer";
|
||||
import Highlight, { defaultProps } from "prism-react-renderer";
|
||||
import { forwardRef, useCallback, useState } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "../primitives/Tooltip";
|
||||
import {
|
||||
ClipboardDocumentCheckIcon,
|
||||
ClipboardIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { ClipboardDocumentCheckIcon, ClipboardIcon } from "@heroicons/react/24/solid";
|
||||
import { Clipboard, ClipboardCheck, ClipboardCheckIcon } from "lucide-react";
|
||||
|
||||
//This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx
|
||||
@@ -193,9 +185,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
const maxLineWidth = lineCount.toString().length;
|
||||
let maxHeight: string | undefined = undefined;
|
||||
if (maxLines && lineCount > maxLines) {
|
||||
maxHeight = `calc(${
|
||||
(maxLines + extraLinesWhenClipping) * 0.75 * 1.625
|
||||
}rem + 1.5rem )`;
|
||||
maxHeight = `calc(${(maxLines + extraLinesWhenClipping) * 0.75 * 1.625}rem + 1.5rem )`;
|
||||
}
|
||||
|
||||
const highlightLines = highlightedRanges?.flatMap(([start, end]) =>
|
||||
@@ -204,10 +194,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-md border border-slate-800",
|
||||
className
|
||||
)}
|
||||
className={cn("relative overflow-hidden rounded-md border border-slate-800", className)}
|
||||
style={{
|
||||
backgroundColor: theme.plain.backgroundColor,
|
||||
}}
|
||||
@@ -226,9 +213,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
className={cn(
|
||||
"absolute right-3 z-50 transition-colors duration-100 hover:cursor-pointer",
|
||||
showChrome ? "top-10" : "top-3",
|
||||
copied
|
||||
? "text-emerald-500"
|
||||
: "text-slate-500 hover:text-slate-300"
|
||||
copied ? "text-emerald-500" : "text-slate-500 hover:text-slate-300"
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
@@ -244,12 +229,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
<Highlight
|
||||
{...defaultProps}
|
||||
theme={theme}
|
||||
code={code}
|
||||
language={language}
|
||||
>
|
||||
<Highlight {...defaultProps} theme={theme} code={code} language={language}>
|
||||
{({
|
||||
className: inheritedClassName,
|
||||
style: inheritedStyle,
|
||||
@@ -285,15 +265,10 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
const lineNumber = index + 1;
|
||||
const lineProps = getLineProps({ line, key: index });
|
||||
|
||||
let hasAnyHighlights = highlightLines
|
||||
? highlightLines.length > 0
|
||||
: false;
|
||||
let hasAnyHighlights = highlightLines ? highlightLines.length > 0 : false;
|
||||
|
||||
let shouldDim = hasAnyHighlights;
|
||||
if (
|
||||
hasAnyHighlights &&
|
||||
highlightLines?.includes(lineNumber)
|
||||
) {
|
||||
if (hasAnyHighlights && highlightLines?.includes(lineNumber)) {
|
||||
shouldDim = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import type { ViewUpdate } from "@codemirror/view";
|
||||
import type {
|
||||
ReactCodeMirrorProps,
|
||||
UseCodeMirror,
|
||||
} from "@uiw/react-codemirror";
|
||||
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
|
||||
import { useCodeMirror } from "@uiw/react-codemirror";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { getEditorSetup } from "./codeMirrorSetup";
|
||||
|
||||
@@ -30,10 +30,7 @@ export function getViewerSetup(): Array<Extension> {
|
||||
return [drawSelection(), dropCursor(), bracketMatching(), lineNumbers()];
|
||||
}
|
||||
|
||||
export function getEditorSetup(
|
||||
showLineNumbers = true,
|
||||
showHighlights = true
|
||||
): Array<Extension> {
|
||||
export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array<Extension> {
|
||||
const options = [
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
|
||||
@@ -36,8 +36,9 @@ export function darkTheme(): Extension {
|
||||
},
|
||||
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||
{ backgroundColor: selection },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
".cm-panels": { backgroundColor: darkBackground, color: ivory },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
@@ -101,13 +102,7 @@ export function darkTheme(): Extension {
|
||||
const jsonHeroHighlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: violet },
|
||||
{
|
||||
tag: [
|
||||
tags.name,
|
||||
tags.deleted,
|
||||
tags.character,
|
||||
tags.propertyName,
|
||||
tags.macroName,
|
||||
],
|
||||
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||
color: coral,
|
||||
},
|
||||
{ tag: [tags.function(tags.variableName), tags.labelName], color: malibu },
|
||||
@@ -194,8 +189,9 @@ export function lightTheme(): Extension[] {
|
||||
},
|
||||
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||
{ backgroundColor: selection },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
".cm-panels": { backgroundColor: darkBackground, color: ivory },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
@@ -259,13 +255,7 @@ export function lightTheme(): Extension[] {
|
||||
const jsonHeroHighlightStyle = tagHighlighter([
|
||||
{ tag: tags.keyword, class: violet },
|
||||
{
|
||||
tag: [
|
||||
tags.name,
|
||||
tags.deleted,
|
||||
tags.character,
|
||||
tags.propertyName,
|
||||
tags.macroName,
|
||||
],
|
||||
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||
class: variableColor,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,97 +1,41 @@
|
||||
import { Paragraph, TextLink } from "~/components/primitives/Paragraph";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import integrationButton from "./integration-button.png";
|
||||
import ngrok from "./ngrok.png";
|
||||
import publicUrl from "./public-url.png";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
|
||||
import { jobTestPath } from "~/utils/pathBuilder";
|
||||
import { 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";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { Callout, variantClasses } from "../primitives/Callout";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsContent,
|
||||
ClientTabsList,
|
||||
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";
|
||||
|
||||
export function HowToSetupYourProject() {
|
||||
const devEnvironment = useDevEnvironment();
|
||||
const appOrigin = useAppOrigin();
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Run your Next.js app" />
|
||||
<StepNumber stepNumber="1" title="Run the CLI 'init' command in a Next.js project" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Ensure your Next.js app is running locally on port 3000.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Create a tunnel to your Next.js app" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
There are a few ways to do this, but we recommend using{" "}
|
||||
<TextLink
|
||||
href="https://trigger.dev/docs/documentation/guides/tunneling-localhost"
|
||||
target="_blank"
|
||||
>
|
||||
ngrok
|
||||
</TextLink>
|
||||
. It’s free and easy to use and required to create a tunnel, making
|
||||
your local machine accessible to the internet.
|
||||
</Paragraph>
|
||||
<StepNumber stepNumber="a" title="Install ngrok" className="mb-2" />
|
||||
<StepContentContainer>
|
||||
<ClientTabs defaultValue="mac">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"mac"}>Mac</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"windows"}>Windows</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"mac"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
fullWidth={false}
|
||||
value={`brew install ngrok/ngrok/ngrok`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"windows"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
fullWidth={false}
|
||||
value={`choco install ngrok`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="b" title="Open a new terminal window/tab" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>You need to leave this running.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="c" title="Create an http tunnel at port 3000" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
This creates a tunnel to your Next.js app.
|
||||
</Paragraph>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
fullWidth={false}
|
||||
className="mb-4"
|
||||
value={`ngrok http 3000`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="d"
|
||||
title={`Grab your "forwarding" URL from the ngrok output`}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<img src={ngrok} className="mt-2 w-full" />
|
||||
</StepContentContainer>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Use the CLI" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Copy this CLI command into a new terminal window.{" "}
|
||||
</Paragraph>
|
||||
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
@@ -102,46 +46,98 @@ export function HowToSetupYourProject() {
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure="npx @trigger.dev/init@latest -k ••••••••• -t https://cloud.trigger.dev -u <ngrok_forwarding_url>"
|
||||
value={`npx @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t https://cloud.trigger.dev -u <ngrok_forwarding_url>`}
|
||||
secure={`npx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`npx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure="pnpm dlx @trigger.dev/init@latest -k ••••••••• -t https://cloud.trigger.dev -u <ngrok_forwarding_url>"
|
||||
value={`pnpm dlx @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t https://cloud.trigger.dev -u <ngrok_forwarding_url>`}
|
||||
secure={`pnpm dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`pnpm dlx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure="yarn @trigger.dev/init@latest -k ••••••••• -t https://cloud.trigger.dev -u <ngrok_forwarding_url>"
|
||||
value={`yarn @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t https://cloud.trigger.dev -u <ngrok_forwarding_url>`}
|
||||
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>
|
||||
Use the "forwarding" URL from step 2d above to replace the -u
|
||||
placeholder text.
|
||||
</Paragraph>
|
||||
<img src={publicUrl} className="mb-4 mt-2 w-full" />
|
||||
</StepContentContainer>
|
||||
|
||||
<StepNumber stepNumber="4" title="Run the CLI" />
|
||||
<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 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" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
It will add Trigger.dev to your existing Next.js project, setup a
|
||||
route and give you an example file.
|
||||
In a <strong className="text-bright">separate terminal window or tab</strong> run:
|
||||
</Paragraph>
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`npx @trigger.dev/cli@latest dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm dlx @trigger.dev/cli@latest dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn 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="5" title="Check for Jobs" />
|
||||
<StepNumber stepNumber="4" title="Check for Jobs" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Once you've run the CLI command, click Refresh to view your example
|
||||
Job in the list.
|
||||
Once you've run the CLI command, click Refresh to view your example Job in the list.
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant="primary/medium"
|
||||
@@ -156,13 +152,91 @@ export function HowToSetupYourProject() {
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToRunYourJob() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const job = useJob();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Callout variant="info" className="mb-6">
|
||||
<Paragraph variant={"small"} className={variantClasses.info.textColor}>
|
||||
Scheduled Triggers <strong>do not</strong> trigger Jobs in the DEV Environment. When
|
||||
developing locally you should use the{" "}
|
||||
<Link
|
||||
to={jobTestPath(organization, project, job)}
|
||||
className="underline underline-offset-2 transition hover:text-blue-100"
|
||||
>
|
||||
Test feature
|
||||
</Link>{" "}
|
||||
to trigger any scheduled Jobs.
|
||||
</Paragraph>
|
||||
</Callout>
|
||||
|
||||
<Paragraph spacing>There are two ways to run your Job:</Paragraph>
|
||||
|
||||
<StepNumber stepNumber="1" title="Trigger a test Run" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
You can perform a Run with any payload you want, or use one of our examples, on the test
|
||||
page.
|
||||
</Paragraph>
|
||||
<LinkButton
|
||||
to={jobTestPath(organization, project, job)}
|
||||
variant={"primary/small"}
|
||||
LeadingIcon={"beaker"}
|
||||
leadingIconClassName="text-bright"
|
||||
>
|
||||
Test
|
||||
</LinkButton>
|
||||
</StepContentContainer>
|
||||
|
||||
<StepNumber stepNumber="2" title="Trigger your Job for real" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Performing a real run depends on the type of Trigger your Job is using.
|
||||
</Paragraph>
|
||||
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/running-jobs"
|
||||
variant={"primary/small"}
|
||||
LeadingIcon={"docs"}
|
||||
leadingIconClassName="text-bright"
|
||||
>
|
||||
How to run a Job
|
||||
</LinkButton>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToRunATest() {
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Step 1 title" />
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Select an environment
|
||||
"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph>Content</Paragraph>
|
||||
<Paragraph spacing>Select the environment you’d like the test to run against.</Paragraph>
|
||||
<img src={selectEnvironment} className="mt-2 w-52" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Write your test payload" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Write your own payload specific to your Job. Some Triggers also provide example payloads
|
||||
that you can select from. This will populate the code editor below.
|
||||
</Paragraph>
|
||||
<img src={selectExample} className="mt-2 h-40" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run your test" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>When you’re happy with the payload, click Run test.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<Callout variant="docs" to="https://trigger.dev/docs/documentation/guides/testing-jobs">
|
||||
Learn more about running tests.
|
||||
</Callout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -174,55 +248,142 @@ export function HowToConnectAnIntegration() {
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
APIs marked with a
|
||||
<span
|
||||
className="mx-2 -mt-1 inline-flex"
|
||||
aria-label="Trigger.dev Integration icon"
|
||||
>
|
||||
<span className="mx-2 -mt-1 inline-flex" aria-label="Trigger.dev Integration icon">
|
||||
<IntegrationIcon />
|
||||
</span>
|
||||
are Trigger.dev Integrations. These Integrations make connecting to
|
||||
the API easier by offering OAuth or API key authentication. All APIs
|
||||
can also be used with fetch or an SDK.
|
||||
are Trigger.dev Integrations. These Integrations make connecting to the API easier by
|
||||
offering OAuth or API key authentication. All APIs can also be used with fetch or an SDK.
|
||||
</Paragraph>
|
||||
<img src={integrationButton} className="mt-2 h-10" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Choose how you want to connect" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Follow the instructions for your chosen connection method in the
|
||||
popover form. If no Integration exists yet, you can request one by
|
||||
clicking the "I want an Integration" button.
|
||||
Follow the instructions for your chosen connection method in the popover form. If no
|
||||
Integration exists yet, you can request one by clicking the "I want an Integration"
|
||||
button.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="3"
|
||||
title="Your connection will appear in the list"
|
||||
/>
|
||||
<StepNumber stepNumber="3" title="Your connection will appear in the list" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Once you've connected your API, it will appear in the list of
|
||||
Integrations below. You can view details and manage your connection by
|
||||
selecting it from the table.
|
||||
Once you've connected your API, it will appear in the list of Integrations below. You can
|
||||
view details and manage your connection by selecting it from the table.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<Callout
|
||||
variant={"docs"}
|
||||
href="https://trigger.dev/docs/integrations/introduction"
|
||||
>
|
||||
View the Integration docs page for more information on connecting an API
|
||||
using an Integration or other method.
|
||||
<Callout variant={"docs"} to="https://trigger.dev/docs/integrations/introduction">
|
||||
View the Integration docs page for more information on connecting an API using an
|
||||
Integration or other method.
|
||||
</Callout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToUseThisIntegration() {
|
||||
export function HowToUseThisIntegration({ integration, help, integrationClient }: HelpPanelProps) {
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Step 1 title" />
|
||||
<StepNumber stepNumber="1" title="Install the package" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>Content</Paragraph>
|
||||
<HelpInstall packageName={integration.packageName} />
|
||||
</StepContentContainer>
|
||||
{help && (
|
||||
<>
|
||||
<StepNumber stepNumber="2" title="Create a Job" />
|
||||
<StepContentContainer>
|
||||
<HelpSamples
|
||||
help={help}
|
||||
integration={integration}
|
||||
integrationClient={integrationClient}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -1,48 +1,34 @@
|
||||
import {
|
||||
ApiAuthenticationMethodApiKey,
|
||||
Integration,
|
||||
} from "~/services/externalApis/types";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Help, Integration } from "~/services/externalApis/types";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import { Header1, Header2 } from "../primitives/Headers";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { HelpInstall } from "./HelpInstall";
|
||||
import { HelpSamples, ReplacementData } from "./HelpSamples";
|
||||
|
||||
export function ApiKeyHelp({
|
||||
integration,
|
||||
apiAuth,
|
||||
}: {
|
||||
integration: Integration;
|
||||
apiAuth: ApiAuthenticationMethodApiKey;
|
||||
}) {
|
||||
export type HelpPanelIntegration = Pick<Integration, "name" | "packageName" | "identifier">;
|
||||
|
||||
export type HelpPanelProps = {
|
||||
integration: HelpPanelIntegration;
|
||||
help?: Help;
|
||||
integrationClient?: ReplacementData;
|
||||
};
|
||||
|
||||
export function ApiKeyHelp({ integration, help, integrationClient }: HelpPanelProps) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Header1 className="mb-2">
|
||||
How to use {integration.name} with API keys
|
||||
</Header1>
|
||||
<Header1 className="mb-2">How to use {integration.name} with API keys</Header1>
|
||||
<Paragraph spacing>
|
||||
You can use API keys to authenticate with {integration.name}. Your API
|
||||
keys won't leave your server, we'll never see them.
|
||||
You can use API keys to authenticate with {integration.name}. Your API keys won't leave your
|
||||
server, we'll never see them.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
First install the <InlineCode>{integration.packageName}</InlineCode>{" "}
|
||||
package using your preferred package manager. For example:
|
||||
First install the <InlineCode>{integration.packageName}</InlineCode> package using your
|
||||
preferred package manager. For example:
|
||||
</Paragraph>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`npm install ${integration.packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
{apiAuth.help.samples.map((sample, i) => (
|
||||
<div key={i}>
|
||||
<Paragraph spacing>{sample.title}</Paragraph>
|
||||
<CodeBlock
|
||||
code={sample.code}
|
||||
className="mb-4"
|
||||
highlightedRanges={sample.highlight}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<HelpInstall packageName={integration.packageName} />
|
||||
{help && (
|
||||
<HelpSamples help={help} integration={integration} integrationClient={integrationClient} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ApiAuthenticationMethodApiKey,
|
||||
Integration,
|
||||
} from "~/services/externalApis/types";
|
||||
import { ApiAuthenticationMethodApiKey, Integration } from "~/services/externalApis/types";
|
||||
import { docsIntegrationPath } from "~/utils/pathBuilder";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { Header1, Header2 } from "../primitives/Headers";
|
||||
import { NamedIconInBox } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTrigger,
|
||||
} from "../primitives/Sheet";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
|
||||
import { ApiKeyHelp } from "./ApiKeyHelp";
|
||||
import { CustomHelp } from "./CustomHelp";
|
||||
import { SelectOAuthMethod } from "./SelectOAuthMethod";
|
||||
@@ -28,16 +19,18 @@ 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
|
||||
>(undefined);
|
||||
const [integrationMethod, setIntegrationMethod] = useState<IntegrationMethod | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const authMethods = Object.values(integration.authenticationMethods);
|
||||
const hasApiKeyOption = authMethods.some((s) => s.type === "apikey");
|
||||
@@ -48,9 +41,9 @@ 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">
|
||||
<Header2>{integration.name}</Header2>
|
||||
<Header1>{integration.name}</Header1>
|
||||
{integration.description && (
|
||||
<Paragraph variant="small">{integration.description}</Paragraph>
|
||||
)}
|
||||
@@ -59,6 +52,7 @@ export function ConnectToIntegrationSheet({
|
||||
to={docsIntegrationPath(integration.identifier)}
|
||||
variant="secondary/small"
|
||||
LeadingIcon="docs"
|
||||
target="_blank"
|
||||
>
|
||||
View docs
|
||||
</LinkButton>
|
||||
@@ -129,12 +123,7 @@ function SelectedIntegrationMethod({
|
||||
case "apikey":
|
||||
const apiAuth = authMethods.find((a) => a.type === "apikey");
|
||||
if (!apiAuth) return null;
|
||||
return (
|
||||
<ApiKeyHelp
|
||||
integration={integration}
|
||||
apiAuth={apiAuth as ApiAuthenticationMethodApiKey}
|
||||
/>
|
||||
);
|
||||
return <ApiKeyHelp integration={integration} help={apiAuth.help} />;
|
||||
case "oauth2":
|
||||
return (
|
||||
<SelectOAuthMethod
|
||||
|
||||
@@ -8,11 +8,7 @@ import simplur from "simplur";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useTextFilter } from "~/hooks/useTextFilter";
|
||||
import { createSchema } from "~/routes/resources.connection.$organizationId.oauth2";
|
||||
import {
|
||||
ApiAuthenticationMethodOAuth2,
|
||||
Integration,
|
||||
Scope,
|
||||
} from "~/services/externalApis/types";
|
||||
import { ApiAuthenticationMethodOAuth2, Integration, Scope } from "~/services/externalApis/types";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
@@ -48,32 +44,22 @@ export function ConnectToOAuthForm({
|
||||
const fetcher = useFetcher();
|
||||
const { isManagedCloud } = useFeatures();
|
||||
|
||||
const [
|
||||
form,
|
||||
{
|
||||
title,
|
||||
slug,
|
||||
scopes,
|
||||
hasCustomClient,
|
||||
customClientId,
|
||||
customClientSecret,
|
||||
},
|
||||
] = useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
// Create the schema without any constraint defined
|
||||
schema: createSchema(),
|
||||
});
|
||||
},
|
||||
});
|
||||
const [form, { title, slug, scopes, hasCustomClient, customClientId, customClientSecret }] =
|
||||
useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
shouldRevalidate: "onSubmit",
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
// Create the schema without any constraint defined
|
||||
schema: createSchema(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const [selectedScopes, setSelectedScopes] = useState<Set<string>>(
|
||||
new Set(
|
||||
authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name)
|
||||
)
|
||||
new Set(authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name))
|
||||
);
|
||||
|
||||
const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud;
|
||||
@@ -84,10 +70,7 @@ export function ConnectToOAuthForm({
|
||||
items: authMethod.scopes,
|
||||
filter: (scope, text) => {
|
||||
if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (
|
||||
scope.description &&
|
||||
scope.description.toLowerCase().includes(text.toLowerCase())
|
||||
)
|
||||
if (scope.description && scope.description.toLowerCase().includes(text.toLowerCase()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@@ -103,16 +86,8 @@ export function ConnectToOAuthForm({
|
||||
>
|
||||
<Fieldset>
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="integrationIdentifier"
|
||||
value={integration.identifier}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="integrationAuthMethod"
|
||||
value={authMethodKey}
|
||||
/>
|
||||
<input type="hidden" name="integrationIdentifier" value={integration.identifier} />
|
||||
<input type="hidden" name="integrationAuthMethod" value={authMethodKey} />
|
||||
<input type="hidden" name="redirectTo" value={location.pathname} />
|
||||
<InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
@@ -126,8 +101,8 @@ export function ConnectToOAuthForm({
|
||||
placeholder={`e.g. ${integration.identifier}`}
|
||||
/>
|
||||
<Hint>
|
||||
This is used in your code to reference this connection. It must be
|
||||
unique for this project.
|
||||
This is used in your code to reference this connection. It must be unique for this
|
||||
project.
|
||||
</Hint>
|
||||
<FormError>{slug.error}</FormError>
|
||||
</InputGroup>
|
||||
@@ -145,8 +120,7 @@ export function ConnectToOAuthForm({
|
||||
<div>
|
||||
<Header2>Use my OAuth App</Header2>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
To use your own OAuth app, check the option below and insert the
|
||||
details.
|
||||
To use your own OAuth app, check the option below and insert the details.
|
||||
</Paragraph>
|
||||
<Checkbox
|
||||
id="hasCustomClient"
|
||||
@@ -160,17 +134,13 @@ export function ConnectToOAuthForm({
|
||||
{useMyOAuthApp && (
|
||||
<div className="ml-6 mt-2">
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
Set the callback url to{" "}
|
||||
<CodeBlock code={callbackUrl} showLineNumbers={false} />
|
||||
Set the callback url to <CodeBlock code={callbackUrl} showLineNumbers={false} />
|
||||
</Paragraph>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client ID</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientId, { type: "text" })}
|
||||
/>
|
||||
<Input fullWidth {...conform.input(customClientId, { type: "text" })} />
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client secret</Label>
|
||||
@@ -187,15 +157,15 @@ 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 +175,59 @@ 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 +237,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>
|
||||
|
||||
@@ -5,20 +5,16 @@ import { Paragraph } from "../primitives/Paragraph";
|
||||
export function CustomHelp({ name }: { name: string }) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Header1 className="mb-2">
|
||||
You can use any API with requests or an SDK
|
||||
</Header1>
|
||||
<Header1 className="mb-2">You can use any API with requests or an SDK</Header1>
|
||||
<Header2 className="mb-2">How to use an SDK</Header2>
|
||||
<Paragraph spacing>
|
||||
You can call SDK methods from inside the run function, but you should
|
||||
wrap them in a Task to make sure they're resumable.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
Here's an example with the official GitHub SDK
|
||||
You can call SDK methods from inside the run function, but you should wrap them in a Task to
|
||||
make sure they're resumable.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>Here's an example with the official GitHub SDK</Paragraph>
|
||||
<CodeBlock
|
||||
code={`
|
||||
new Job(client, {
|
||||
client.defineJob({
|
||||
id: "scheduled-job-1",
|
||||
name: "Scheduled Job 1",
|
||||
version: "0.1.1",
|
||||
@@ -48,13 +44,12 @@ new Job(client, {
|
||||
/>
|
||||
<Header2 className="mb-2">How to use fetch</Header2>
|
||||
<Paragraph spacing>
|
||||
You can use the fetch API to make requests to any API. Or a different
|
||||
request library like axios if you'd prefer. Again wrapping the request
|
||||
in a Task will make sure it's resumable.
|
||||
You can use the fetch API to make requests to any API. Or a different request library like
|
||||
axios if you'd prefer. Again wrapping the request in a Task will make sure it's resumable.
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
code={`
|
||||
new Job(client, {
|
||||
client.defineJob({
|
||||
id: "scheduled-job-1",
|
||||
name: "Scheduled Job 1",
|
||||
version: "0.1.1",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Integration } from "~/services/externalApis/types";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
ClientTabsContent,
|
||||
} from "../primitives/ClientTabs";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
export function HelpInstall({ packageName }: { packageName: string }) {
|
||||
return (
|
||||
<>
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`npm install ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`pnpm add ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`yarn add ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Integration } from "~/services/externalApis/types";
|
||||
import { HelpPanelIntegration, HelpPanelProps } from "./ApiKeyHelp";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
|
||||
export type ReplacementData = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export function HelpSamples({ help, integrationClient, integration }: HelpPanelProps) {
|
||||
return (
|
||||
<>
|
||||
{help &&
|
||||
help.samples.map((sample, i) => {
|
||||
const code = runReplacers(sample.code, integrationClient, integration);
|
||||
return (
|
||||
<div key={i}>
|
||||
<Paragraph spacing>{sample.title}</Paragraph>
|
||||
<CodeBlock code={code} className="mb-4" highlightedRanges={sample.highlight} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const replacements = [
|
||||
{
|
||||
match: /__SLUG__/g,
|
||||
replacement: (data: ReplacementData | undefined, integration: HelpPanelIntegration) => {
|
||||
if (data) return data.slug;
|
||||
return integration.identifier;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function runReplacers(
|
||||
code: string,
|
||||
replacementData: ReplacementData | undefined,
|
||||
integration: HelpPanelIntegration
|
||||
) {
|
||||
replacements.forEach((r) => {
|
||||
code = code.replace(r.match, r.replacement(replacementData, integration));
|
||||
});
|
||||
|
||||
return code;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { docsIntegrationPath } from "~/utils/pathBuilder";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
import { NamedIconInBox } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
|
||||
import { SelectOAuthMethod } from "./SelectOAuthMethod";
|
||||
import { Integration } from "~/services/externalApis/types";
|
||||
import { Client } from "~/presenters/IntegrationsPresenter.server";
|
||||
|
||||
export function IntegrationWithMissingFieldSheet({
|
||||
integration,
|
||||
organizationId,
|
||||
button,
|
||||
callbackUrl,
|
||||
existingIntegration,
|
||||
className,
|
||||
}: {
|
||||
integration: Integration;
|
||||
organizationId: string;
|
||||
button: React.ReactNode;
|
||||
callbackUrl: string;
|
||||
existingIntegration: Client;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger className={className}>{button}</SheetTrigger>
|
||||
<SheetContent size="lg" className="relative">
|
||||
<SheetHeader>
|
||||
<NamedIconInBox name={integration.identifier} className="h-9 w-9" />
|
||||
<div className="grow">
|
||||
<Header1>{integration.name}</Header1>
|
||||
{integration.description && (
|
||||
<Paragraph variant="small">{integration.description}</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
<LinkButton
|
||||
to={docsIntegrationPath(integration.identifier)}
|
||||
variant="secondary/small"
|
||||
LeadingIcon="docs"
|
||||
target="_blank"
|
||||
>
|
||||
View docs
|
||||
</LinkButton>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<SelectOAuthMethod
|
||||
integration={integration}
|
||||
organizationId={organizationId}
|
||||
callbackUrl={callbackUrl}
|
||||
existingIntegration={existingIntegration}
|
||||
/>
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -4,13 +4,7 @@ import { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
import { NamedIconInBox } from "../primitives/NamedIcon";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTrigger,
|
||||
} from "../primitives/Sheet";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
|
||||
import { CustomHelp } from "./CustomHelp";
|
||||
import { CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
@@ -41,32 +35,22 @@ export function NoIntegrationSheet({
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckIcon className="h-4 w-4 text-green-500" />
|
||||
<Paragraph variant="small">
|
||||
We'll let you know when the integration is available.
|
||||
We'll let you know when the Integration is available.
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
action={`/resources/apivote/${api.identifier}`}
|
||||
>
|
||||
<fetcher.Form method="post" action={`/resources/apivote/${api.identifier}`}>
|
||||
<Button
|
||||
variant="primary/small"
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? "spinner-white" : undefined}
|
||||
>
|
||||
{isLoading
|
||||
? "Saving…"
|
||||
: `I want an integration for ${api.name}`}
|
||||
{isLoading ? "Saving…" : `I want an Integration for ${api.name}`}
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
)}
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<Callout variant="info">
|
||||
We don’t have an integration for the {api.name} API yet but you can
|
||||
request one by clicking the button above. In the meantime, connect
|
||||
to {api.name} using the methods below.
|
||||
</Callout>
|
||||
<CustomHelp name={api.name} />
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ApiAuthenticationMethodOAuth2,
|
||||
Integration,
|
||||
} from "~/services/externalApis/types";
|
||||
import { ApiAuthenticationMethodOAuth2, Integration } from "~/services/externalApis/types";
|
||||
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
|
||||
import type { ConnectionType } from "@trigger.dev/database";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { ConnectToOAuthForm } from "./ConnectToOAuthForm";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Client } from "~/presenters/IntegrationsPresenter.server";
|
||||
import { UpdateOAuthForm } from "./UpdateOAuthForm";
|
||||
|
||||
export function SelectOAuthMethod({
|
||||
integration,
|
||||
organizationId,
|
||||
callbackUrl
|
||||
callbackUrl,
|
||||
existingIntegration,
|
||||
}: {
|
||||
existingIntegration?: Client;
|
||||
integration: Integration;
|
||||
organizationId: string;
|
||||
callbackUrl: string;
|
||||
@@ -25,14 +26,10 @@ export function SelectOAuthMethod({
|
||||
const [oAuthKey, setOAuthKey] = useState<string | undefined>(
|
||||
oAuthMethods.length === 1 ? oAuthMethods[0][0] : undefined
|
||||
);
|
||||
const [connectionType, setConnectionType] = useState<
|
||||
ConnectionType | undefined
|
||||
>();
|
||||
const [connectionType, setConnectionType] = useState<ConnectionType | undefined>();
|
||||
|
||||
const selectedOAuthMethod = oAuthKey
|
||||
? (integration.authenticationMethods[
|
||||
oAuthKey
|
||||
] as ApiAuthenticationMethodOAuth2)
|
||||
? (integration.authenticationMethods[oAuthKey] as ApiAuthenticationMethodOAuth2)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
@@ -61,9 +58,7 @@ export function SelectOAuthMethod({
|
||||
)}
|
||||
{selectedOAuthMethod && (
|
||||
<>
|
||||
<Header2 className="mb-2 mt-4">
|
||||
Who is connecting to {integration.name}
|
||||
</Header2>
|
||||
<Header2 className="mb-2 mt-4">Who is connecting to {integration.name}</Header2>
|
||||
<RadioGroup
|
||||
name="connection-type"
|
||||
className="flex gap-2"
|
||||
@@ -91,24 +86,33 @@ export function SelectOAuthMethod({
|
||||
connectionType &&
|
||||
oAuthKey &&
|
||||
(connectionType === "DEVELOPER" ? (
|
||||
<ConnectToOAuthForm
|
||||
integration={integration}
|
||||
authMethod={selectedOAuthMethod}
|
||||
authMethodKey={oAuthKey}
|
||||
organizationId={organizationId}
|
||||
clientType={connectionType}
|
||||
callbackUrl={callbackUrl}
|
||||
/>
|
||||
existingIntegration ? (
|
||||
<UpdateOAuthForm
|
||||
existingIntegration={existingIntegration}
|
||||
integration={integration}
|
||||
authMethod={selectedOAuthMethod}
|
||||
authMethodKey={oAuthKey}
|
||||
organizationId={organizationId}
|
||||
clientType={connectionType}
|
||||
callbackUrl={callbackUrl}
|
||||
/>
|
||||
) : (
|
||||
<ConnectToOAuthForm
|
||||
integration={integration}
|
||||
authMethod={selectedOAuthMethod}
|
||||
authMethodKey={oAuthKey}
|
||||
organizationId={organizationId}
|
||||
clientType={connectionType}
|
||||
callbackUrl={callbackUrl}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Header2 className="mb-1 mt-4">User OAuth coming soon</Header2>
|
||||
<Paragraph spacing>
|
||||
End-user OAuth is going to be released soon. If you are interested
|
||||
in being an early beta tester then please{" "}
|
||||
<a
|
||||
href="mailto:founders@trigger.dev"
|
||||
className="text-indigo-500 underline"
|
||||
>
|
||||
End-user OAuth is going to be released soon. If you are interested in being an early
|
||||
beta tester then please{" "}
|
||||
<a href="mailto:founders@trigger.dev" className="text-indigo-500 underline">
|
||||
message us
|
||||
</a>
|
||||
.
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { useFetcher, useLocation, useNavigation } from "@remix-run/react";
|
||||
import type { ConnectionType } from "@trigger.dev/database";
|
||||
import { useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useTextFilter } from "~/hooks/useTextFilter";
|
||||
import { ApiAuthenticationMethodOAuth2, Integration, Scope } from "~/services/externalApis/types";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Checkbox } from "../primitives/Checkbox";
|
||||
import { Fieldset } from "../primitives/Fieldset";
|
||||
import { FormError } from "../primitives/FormError";
|
||||
import { Header2, Header3 } from "../primitives/Headers";
|
||||
import { Input } from "../primitives/Input";
|
||||
import { InputGroup } from "../primitives/InputGroup";
|
||||
import { Label } from "../primitives/Label";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Client } from "~/presenters/IntegrationsPresenter.server";
|
||||
import { schema } from "~/routes/resources.connection.$organizationId.oauth2.$integrationId";
|
||||
|
||||
export type Status = "loading" | "idle";
|
||||
|
||||
export function UpdateOAuthForm({
|
||||
existingIntegration,
|
||||
integration,
|
||||
authMethod,
|
||||
authMethodKey,
|
||||
organizationId,
|
||||
clientType,
|
||||
callbackUrl,
|
||||
}: {
|
||||
existingIntegration: Client;
|
||||
integration: Integration;
|
||||
authMethod: ApiAuthenticationMethodOAuth2;
|
||||
authMethodKey: string;
|
||||
organizationId: string;
|
||||
clientType: ConnectionType;
|
||||
callbackUrl: string;
|
||||
}) {
|
||||
const transition = useNavigation();
|
||||
const fetcher = useFetcher();
|
||||
const { isManagedCloud } = useFeatures();
|
||||
|
||||
const [form, { title, scopes, hasCustomClient, customClientId, customClientSecret }] = useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
schema,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const [selectedScopes, setSelectedScopes] = useState<Set<string>>(
|
||||
new Set(authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name))
|
||||
);
|
||||
|
||||
const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud;
|
||||
|
||||
const [useMyOAuthApp, setUseMyOAuthApp] = useState(requiresCustomOAuthApp);
|
||||
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<Scope>({
|
||||
items: authMethod.scopes,
|
||||
filter: (scope, text) => {
|
||||
if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (scope.description && scope.description.toLowerCase().includes(text.toLowerCase()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<fetcher.Form
|
||||
method="put"
|
||||
action={`/resources/connection/${organizationId}/oauth2/${existingIntegration.id}`}
|
||||
{...form.props}
|
||||
className="flex h-full max-h-full flex-grow flex-col"
|
||||
>
|
||||
<Fieldset>
|
||||
<input type="hidden" name="id" value={existingIntegration.id} />
|
||||
<input type="hidden" name="integrationIdentifier" value={integration.identifier} />
|
||||
<input type="hidden" name="integrationAuthMethod" value={authMethodKey} />
|
||||
<input type="hidden" name="redirectTo" value={location.pathname} />
|
||||
<InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">ID</Label>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
{existingIntegration.slug}
|
||||
</Paragraph>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
{...conform.input(title)}
|
||||
defaultValue={existingIntegration.title ?? undefined}
|
||||
placeholder={`e.g. Personal ${integration.name}`}
|
||||
/>
|
||||
<FormError>{title.error}</FormError>
|
||||
</InputGroup>
|
||||
<input type="hidden" name="clientType" value={clientType} />
|
||||
<div>
|
||||
<Header2>Use my OAuth App</Header2>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
To use your own OAuth app, check the option below and insert the details.
|
||||
</Paragraph>
|
||||
<Checkbox
|
||||
id="hasCustomClient"
|
||||
label="Use my OAuth App"
|
||||
variant="simple/small"
|
||||
disabled={requiresCustomOAuthApp}
|
||||
onChange={(checked) => setUseMyOAuthApp(checked)}
|
||||
{...conform.input(hasCustomClient, { type: "checkbox" })}
|
||||
defaultChecked={requiresCustomOAuthApp}
|
||||
/>
|
||||
{useMyOAuthApp && (
|
||||
<div className="ml-6 mt-2">
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
Set the callback url to <CodeBlock code={callbackUrl} showLineNumbers={false} />
|
||||
</Paragraph>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client ID</Label>
|
||||
<Input fullWidth {...conform.input(customClientId, { type: "text" })} />
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client secret</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientSecret, {
|
||||
type: "password",
|
||||
})}
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<FormError>{customClientId.error}</FormError>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{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>
|
||||
<Checkbox
|
||||
id="allScopes"
|
||||
label="Select all"
|
||||
variant="button/small"
|
||||
/>
|
||||
</fieldset> */}
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<Header3>Select {integration.name} scopes</Header3>
|
||||
<Paragraph variant="small" className="text-slate-500">
|
||||
{simplur`${selectedScopes.size} scope[|s] selected`}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search scopes"
|
||||
className="mb-2"
|
||||
variant="medium"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="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>
|
||||
)}
|
||||
</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">
|
||||
<FormError>{scopes.error}</FormError>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex gap-2"
|
||||
disabled={transition.state !== "idle"}
|
||||
variant="primary/medium"
|
||||
LeadingIcon={integration.identifier}
|
||||
>
|
||||
Connect to {integration.name}
|
||||
</Button>
|
||||
</div>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
@@ -15,17 +15,13 @@ 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";
|
||||
|
||||
export function JobsTable({
|
||||
jobs,
|
||||
noResultsText,
|
||||
}: {
|
||||
jobs: ProjectJob[];
|
||||
noResultsText: string;
|
||||
}) {
|
||||
export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
|
||||
@@ -46,7 +42,10 @@ export function JobsTable({
|
||||
jobs.map((job) => {
|
||||
const path = jobPath(organization, project, job);
|
||||
return (
|
||||
<TableRow key={job.id}>
|
||||
<TableRow
|
||||
key={job.id}
|
||||
className={cn(job.hasIntegrationsRequiringAction && "bg-rose-500/30")}
|
||||
>
|
||||
<TableCell to={path}>
|
||||
<span className="flex items-center gap-2">
|
||||
<NamedIcon name={job.event.icon} className="h-8 w-8" />
|
||||
@@ -56,8 +55,7 @@ export function JobsTable({
|
||||
job.dynamic ? (
|
||||
<span className="flex items-center gap-0.5">
|
||||
<NamedIcon name="dynamic" className="h-4 w-4" />{" "}
|
||||
<span className="uppercase">Dynamic:</span>{" "}
|
||||
{job.event.title}
|
||||
<span className="uppercase">Dynamic:</span> {job.event.title}
|
||||
</span>
|
||||
) : (
|
||||
job.event.title
|
||||
@@ -68,23 +66,31 @@ export function JobsTable({
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<LabelValueStack
|
||||
label={job.slug}
|
||||
value={`v${job.version}`}
|
||||
variant="primary"
|
||||
/>
|
||||
<LabelValueStack label={job.slug} value={`v${job.version}`} variant="primary" />
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{job.integrations.map((integration) => (
|
||||
<SimpleTooltip
|
||||
key={integration.key}
|
||||
button={
|
||||
<NamedIcon
|
||||
name={integration.icon}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
<div className="relative">
|
||||
<NamedIcon name={integration.icon} className="h-6 w-6" />
|
||||
{integration.setupStatus === "MISSING_FIELDS" && (
|
||||
<NamedIcon name="error" className="absolute -left-1 -top-1 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
content={
|
||||
<div>
|
||||
<p className="mb-1 text-rose-400">
|
||||
{integration.setupStatus === "MISSING_FIELDS" &&
|
||||
"This integration requires configuration"}
|
||||
</p>
|
||||
<p>
|
||||
{integration.title}: {integration.key}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
content={`${integration.title}: ${integration.key}`}
|
||||
/>
|
||||
))}
|
||||
</TableCell>
|
||||
@@ -126,10 +132,9 @@ export function JobsTable({
|
||||
</span>
|
||||
}
|
||||
value={
|
||||
<DateTime
|
||||
date={job.lastRun.createdAt}
|
||||
className={classForJobStatus(job.lastRun.status)}
|
||||
/>
|
||||
<span className={classForJobStatus(job.lastRun.status)}>
|
||||
<DateTime date={job.lastRun.createdAt} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
@@ -142,10 +147,7 @@ export function JobsTable({
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={6}>
|
||||
<Paragraph
|
||||
variant="small"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Paragraph variant="small" className="flex items-center justify-center">
|
||||
{noResultsText}
|
||||
</Paragraph>
|
||||
</TableBlankRow>
|
||||
|
||||
@@ -11,9 +11,7 @@ export function AppContainer({
|
||||
}) {
|
||||
return (
|
||||
<BackgroundGradient showBackgroundGradient={showBackgroundGradient}>
|
||||
<div className={cn("grid h-full w-full grid-rows-[2.75rem_auto]")}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={cn("grid h-full w-full grid-rows-[2.75rem_auto]")}>{children}</div>
|
||||
</BackgroundGradient>
|
||||
);
|
||||
}
|
||||
@@ -43,9 +41,7 @@ export function BackgroundGradient({
|
||||
|
||||
/** This container should be placed around the content on a page */
|
||||
export function PageContainer({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={cn("grid h-full grid-rows-[auto_1fr]")}>{children}</div>
|
||||
);
|
||||
return <div className={cn("grid h-full grid-rows-[auto_1fr]")}>{children}</div>;
|
||||
}
|
||||
|
||||
export function PageBody({
|
||||
@@ -56,11 +52,7 @@ export function PageBody({
|
||||
scrollable?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(scrollable ? "overflow-y-auto p-4 " : "overflow-hidden")}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className={cn(scrollable ? "overflow-y-auto p-4 " : "overflow-hidden")}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,11 +60,7 @@ export function PageBodyPadding({ children }: { children: React.ReactNode }) {
|
||||
return <div className="p-4">{children}</div>;
|
||||
}
|
||||
|
||||
export function MainCenteredContainer({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export function MainCenteredContainer({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="mx-auto mt-[30vh] max-w-xs">{children}</div>
|
||||
|
||||
@@ -1,211 +1,26 @@
|
||||
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":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectPath(organization!, project!)}
|
||||
title="Jobs"
|
||||
/>
|
||||
);
|
||||
case "environments":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectEnvironmentsPath(organization!, project!)}
|
||||
title="Environments"
|
||||
/>
|
||||
);
|
||||
case "integrations":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectIntegrationsPath(organization!, project!)}
|
||||
title="Integrations"
|
||||
/>
|
||||
);
|
||||
case "integration":
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={projectIntegrationsPath(organization!, project!)}
|
||||
title="Integrations"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<BreadcrumbLink
|
||||
to={integrationClientPath(organization!, project!, client!)}
|
||||
title={client!.title}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
case "integration-job":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientPath(organization!, project!, client!)}
|
||||
title={"Jobs"}
|
||||
/>
|
||||
);
|
||||
case "integration-connections":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientConnectionsPath(
|
||||
organization!,
|
||||
project!,
|
||||
client!
|
||||
)}
|
||||
title={"Connections"}
|
||||
/>
|
||||
);
|
||||
case "integration-scopes":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientScopesPath(organization!, project!, client!)}
|
||||
title={"Scopes"}
|
||||
/>
|
||||
);
|
||||
case "job":
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={projectPath(organization!, project!)}
|
||||
title="Jobs"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<JobsMenu key={breadcrumb.slug} />
|
||||
</Fragment>
|
||||
);
|
||||
case "test":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={jobTestPath(organization!, project!, job!)}
|
||||
title="Test"
|
||||
/>
|
||||
);
|
||||
case "runs":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={jobPath(organization!, project!, job!)}
|
||||
title="Runs"
|
||||
/>
|
||||
);
|
||||
case "run":
|
||||
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,26 +1,26 @@
|
||||
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 { IconNames, NamedIcon } from "../primitives/NamedIcon";
|
||||
import { LabelValueStack } from "../primitives/LabelValueStack";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrowTrigger,
|
||||
PopoverContent,
|
||||
PopoverMenuItem,
|
||||
PopoverSectionHeader,
|
||||
} from "../primitives/Popover";
|
||||
import { LabelValueStack } from "../primitives/LabelValueStack";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
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 (
|
||||
<>
|
||||
@@ -29,33 +29,31 @@ export function JobsMenu() {
|
||||
{currentJob?.title ?? "Select a job"}
|
||||
</PopoverArrowTrigger>
|
||||
<PopoverContent
|
||||
className="w-80 overflow-y-auto p-0"
|
||||
className="min-w-[20rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
align="start"
|
||||
collisionPadding={20}
|
||||
>
|
||||
<PopoverSectionHeader title="Jobs" />
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
{project.jobs.map((job) => {
|
||||
{projectJobs.map((job) => {
|
||||
const isSelected = job.id === currentJob?.id;
|
||||
return (
|
||||
<Link
|
||||
key={job.id}
|
||||
to={jobPath(organization, project, job)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 rounded-md p-2",
|
||||
"flex w-full items-center gap-2 rounded-md p-2 transition hover:bg-slate-800",
|
||||
isSelected && "bg-slate-750 group-hover:bg-slate-750"
|
||||
)}
|
||||
>
|
||||
<NamedIcon name={job.event.icon} className="h-6 w-6" />
|
||||
<NamedIcon name={job.event.icon} className="h-8 w-8" />
|
||||
<LabelValueStack
|
||||
label={job.title}
|
||||
value={job.slug}
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
/>
|
||||
{isSelected && (
|
||||
<NamedIcon name="check" className="mr-1 h-6 w-6" />
|
||||
)}
|
||||
{isSelected && <NamedIcon name="check" className="mr-1 h-6 w-6" />}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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";
|
||||
|
||||
export function NavBar() {
|
||||
return (
|
||||
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-divide py-1 pl-2 pr-2.5">
|
||||
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-divide py-1 pl-1 pr-2.5">
|
||||
<div className="flex gap-0.5">
|
||||
<Link to="/" className="p-2">
|
||||
<LogoIcon className="h-5 w-5" />
|
||||
@@ -20,14 +20,20 @@ export function NavBar() {
|
||||
<MobileDropdownMenu />
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
<LinkButton
|
||||
to={docsRoot()}
|
||||
variant="secondary/small"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
>
|
||||
<LinkButton to={docsRoot()} variant="secondary/small" LeadingIcon={BookOpenIcon}>
|
||||
Documentation
|
||||
</LinkButton>
|
||||
<Feedback />
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
LeadingIcon={ChatBubbleLeftRightIcon}
|
||||
shortcut={{ key: "f" }}
|
||||
>
|
||||
Help & feedback
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -115,10 +121,7 @@ function MobileNavIcon({ open }: { open: boolean }) {
|
||||
/>
|
||||
<path
|
||||
d="M2 2L12 12M12 2L2 12"
|
||||
className={cn(
|
||||
"origin-center transition",
|
||||
!open && "scale-90 opacity-0"
|
||||
)}
|
||||
className={cn("origin-center transition", !open && "scale-90 opacity-0")}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { motion } from "framer-motion";
|
||||
import invariant from "tiny-invariant";
|
||||
import { useJob, useOptionalJob } from "~/hooks/useJob";
|
||||
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,19 +12,15 @@ import {
|
||||
projectEnvironmentsPath,
|
||||
projectIntegrationsPath,
|
||||
projectPath,
|
||||
projectTriggersPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { NavLinkButton } from "../primitives/Buttons";
|
||||
import type { IconNames } from "../primitives/NamedIcon";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { useOptionalIntegrationClient } from "~/hooks/useIntegrationClient";
|
||||
import { NavLinkButton } from "../primitives/Buttons";
|
||||
import { NamedIcon, type IconNames } from "../primitives/NamedIcon";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
|
||||
export function SideMenuContainer({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-full w-full justify-stretch overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className="flex h-full w-full justify-stretch overflow-hidden">{children}</div>;
|
||||
}
|
||||
|
||||
const expandedWith = "14rem";
|
||||
@@ -46,15 +42,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 ===
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam._index";
|
||||
deepestMatch?.id === "routes/_app.orgs.$organizationSlug.projects.$projectParam._index";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -72,18 +68,30 @@ export function ProjectSideMenu() {
|
||||
to={projectPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
forceActive={jobsActive}
|
||||
data-action="jobs"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Integrations"
|
||||
icon="integration"
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
hasWarning={project.hasUnconfiguredIntegrations}
|
||||
data-action="integrations"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Triggers"
|
||||
icon="trigger"
|
||||
to={projectTriggersPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
hasWarning={project.hasInactiveExternalTriggers}
|
||||
data-action="triggers"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Environments & API Keys"
|
||||
icon="environment"
|
||||
to={projectEnvironmentsPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="environments & api keys"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
@@ -92,18 +100,21 @@ export function ProjectSideMenu() {
|
||||
icon="team"
|
||||
to={organizationTeamPath(organization)}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="team"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Usage & Billing"
|
||||
icon="billing"
|
||||
to={organizationBillingPath(organization)}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="usage & billing"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Account"
|
||||
icon={UserProfilePhoto}
|
||||
to={accountPath()}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="account"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -125,11 +136,13 @@ function SideMenuItem({
|
||||
to,
|
||||
isCollapsed,
|
||||
forceActive,
|
||||
hasWarning = false,
|
||||
}: {
|
||||
icon: IconNames | React.ComponentType<any>;
|
||||
name: string;
|
||||
to: string;
|
||||
isCollapsed: boolean;
|
||||
hasWarning?: boolean;
|
||||
forceActive?: boolean;
|
||||
}) {
|
||||
return (
|
||||
@@ -147,6 +160,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"
|
||||
@@ -161,6 +175,7 @@ function SideMenuItem({
|
||||
>
|
||||
{name}
|
||||
</motion.span>
|
||||
{hasWarning && <NamedIcon name="error" className="absolute left-1 top-1 h-4 w-4" />}
|
||||
</NavLinkButton>
|
||||
}
|
||||
content={name}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { Fragment, useState } from "react";
|
||||
import {
|
||||
useIsNewOrganizationPage,
|
||||
useOrganizations,
|
||||
} from "~/hooks/useOrganizations";
|
||||
import { useOptionalProject, useProject } from "~/hooks/useProject";
|
||||
import {
|
||||
newOrganizationPath,
|
||||
newProjectPath,
|
||||
projectPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import simplur from "simplur";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { useIsNewOrganizationPage, useOrganizations } from "~/hooks/useOrganizations";
|
||||
import { useOptionalProject } from "~/hooks/useProject";
|
||||
import { newOrganizationPath, newProjectPath, projectPath } from "~/utils/pathBuilder";
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrowTrigger,
|
||||
@@ -17,11 +13,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;
|
||||
@@ -33,18 +29,27 @@ export function ProjectsMenu() {
|
||||
<PopoverArrowTrigger isOpen={isOpen}>
|
||||
{currentProject?.name ?? "Select a project"}
|
||||
</PopoverArrowTrigger>
|
||||
<PopoverContent className="w-80 p-0" align="start">
|
||||
<PopoverContent
|
||||
className="min-w-[20rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
align="start"
|
||||
collisionPadding={20}
|
||||
>
|
||||
{organizations.map((organization) => (
|
||||
<Fragment key={organization.id}>
|
||||
<PopoverSectionHeader title={organization.title} />
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<div className="flex flex-col gap-1 p-1 ">
|
||||
{organization.projects.map((project) => {
|
||||
const isSelected = project.id === currentProject?.id;
|
||||
return (
|
||||
<PopoverMenuItem
|
||||
key={project.id}
|
||||
to={projectPath(organization, project)}
|
||||
title={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"
|
||||
/>
|
||||
|
||||
@@ -54,29 +54,14 @@ const AlertContent = React.forwardRef<
|
||||
));
|
||||
AlertContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
const AlertHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
const AlertHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
AlertHeader.displayName = "AlertDialogHeader";
|
||||
|
||||
const AlertFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
const AlertFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -118,11 +103,7 @@ const AlertCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn("mt-2 sm:mt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
<AlertDialogPrimitive.Cancel ref={ref} className={cn("mt-2 sm:mt-0", className)} {...props} />
|
||||
));
|
||||
AlertCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
|
||||
|
||||
@@ -4,16 +4,14 @@ 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> & {
|
||||
variant?: keyof typeof variants;
|
||||
};
|
||||
|
||||
export function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: BadgeProps) {
|
||||
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
||||
return <div className={cn(variants[variant], className)} {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { Link, LinkProps, NavLink, NavLinkProps } from "@remix-run/react";
|
||||
import React, {
|
||||
ReactComponentElement,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react";
|
||||
import React, { ReactComponentElement, forwardRef, useImperativeHandle, useRef } 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 = {
|
||||
"primary/small": {
|
||||
textColor:
|
||||
"text-bright group-hover:text-white transition group-disabled:text-bright/80",
|
||||
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80",
|
||||
button:
|
||||
"h-6 px-[5px] text-xs bg-indigo-600 group-hover:bg-indigo-500/90 group-disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
icon: "h-3.5",
|
||||
@@ -22,8 +16,7 @@ const variant = {
|
||||
"ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60 justify-self-center",
|
||||
},
|
||||
"secondary/small": {
|
||||
textColor:
|
||||
"text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
|
||||
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
|
||||
button:
|
||||
"h-6 px-[5px] text-xs bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
icon: "h-3.5",
|
||||
@@ -32,8 +25,7 @@ const variant = {
|
||||
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
|
||||
},
|
||||
"tertiary/small": {
|
||||
textColor:
|
||||
"text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
|
||||
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
|
||||
button:
|
||||
"h-6 px-[5px] text-xs bg-transparent group-hover:bg-slate-850 disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
icon: "h-3.5",
|
||||
@@ -42,58 +34,45 @@ const variant = {
|
||||
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
|
||||
},
|
||||
"danger/small": {
|
||||
textColor:
|
||||
"text-bright group-hover:text-white transition group-disabled:text-bright/80",
|
||||
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80",
|
||||
button:
|
||||
"h-6 px-[5px] text-xs bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
icon: "h-3.5",
|
||||
shortcutVariant: "small" as const,
|
||||
shortcut:
|
||||
"ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
shortcut: "ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
},
|
||||
"primary/medium": {
|
||||
textColor:
|
||||
"text-bright group-hover:text-white transition group-disabled:text-bright/80",
|
||||
button:
|
||||
"h-8 px-2 text-sm bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
|
||||
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80",
|
||||
button: "h-8 px-2 text-sm bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
|
||||
icon: "h-4",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut:
|
||||
"ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
},
|
||||
"secondary/medium": {
|
||||
textColor:
|
||||
"text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
|
||||
button:
|
||||
"h-8 px-2 text-sm bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
|
||||
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
|
||||
button: "h-8 px-2 text-sm bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
|
||||
icon: "h-4",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut:
|
||||
"ml-1.5 -mr-0.5 border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright",
|
||||
},
|
||||
"tertiary/medium": {
|
||||
textColor:
|
||||
"text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
|
||||
button:
|
||||
"h-8 px-2 text-sm bg-transparent group-hover:bg-slate-850 disabled:opacity-50",
|
||||
textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80",
|
||||
button: "h-8 px-2 text-sm bg-transparent group-hover:bg-slate-850 disabled:opacity-50",
|
||||
icon: "h-4",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut:
|
||||
"ml-1.5 -mr-0.5 border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright",
|
||||
},
|
||||
"danger/medium": {
|
||||
textColor:
|
||||
"text-bright group-hover:text-white transition group-disabled:text-bright/80",
|
||||
button:
|
||||
"h-8 px-2 text-sm bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50",
|
||||
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80",
|
||||
button: "h-8 px-2 text-sm bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50",
|
||||
icon: "h-4",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut:
|
||||
"ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
},
|
||||
"primary/large": {
|
||||
textColor:
|
||||
"text-bright group-hover:text-white transition group-disabled:text-dimmed/80",
|
||||
textColor: "text-bright group-hover:text-white transition group-disabled:text-dimmed/80",
|
||||
button:
|
||||
"h-10 px-2 text-sm font-medium bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
|
||||
icon: "h-5",
|
||||
@@ -125,8 +104,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;
|
||||
@@ -157,13 +136,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
const textColorClassName = variation.textColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
fullWidth ? "flex" : "inline-flex text-xxs",
|
||||
btnClassName,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cn(fullWidth ? "flex" : "inline-flex text-xxs", btnClassName, className)}>
|
||||
<div
|
||||
className={cn(
|
||||
textAlignLeft ? "text-left" : "justify-center",
|
||||
@@ -174,11 +147,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
(typeof LeadingIcon === "string" ? (
|
||||
<NamedIcon
|
||||
name={LeadingIcon}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
leadingIconClassName,
|
||||
"shrink-0 justify-start"
|
||||
)}
|
||||
className={cn(iconClassName, leadingIconClassName, "shrink-0 justify-start")}
|
||||
/>
|
||||
) : (
|
||||
<LeadingIcon
|
||||
@@ -193,12 +162,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
|
||||
{text &&
|
||||
(typeof text === "string" ? (
|
||||
<span
|
||||
className={cn(
|
||||
"mx-auto grow self-center truncate px-1",
|
||||
textColorClassName
|
||||
)}
|
||||
>
|
||||
<span className={cn("mx-auto grow self-center truncate px-1", textColorClassName)}>
|
||||
{text}
|
||||
</span>
|
||||
) : (
|
||||
@@ -209,11 +173,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
(typeof TrailingIcon === "string" ? (
|
||||
<NamedIcon
|
||||
name={TrailingIcon}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
trailingIconClassName,
|
||||
"shrink-0 justify-end"
|
||||
)}
|
||||
className={cn(iconClassName, trailingIconClassName, "shrink-0 justify-end")}
|
||||
/>
|
||||
) : (
|
||||
<TrailingIcon
|
||||
@@ -277,8 +237,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
}
|
||||
);
|
||||
|
||||
type LinkPropsType = Pick<LinkProps, "to" | "target"> &
|
||||
React.ComponentProps<typeof ButtonContent>;
|
||||
type LinkPropsType = Pick<LinkProps, "to" | "target"> & React.ComponentProps<typeof ButtonContent>;
|
||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||
const innerRef = useRef<HTMLAnchorElement>(null);
|
||||
if (props.shortcut) {
|
||||
@@ -317,26 +276,13 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||
|
||||
type NavLinkPropsType = Pick<NavLinkProps, "to" | "target"> &
|
||||
Omit<React.ComponentProps<typeof ButtonContent>, "className"> & {
|
||||
className?: (props: {
|
||||
isActive: boolean;
|
||||
isPending: boolean;
|
||||
}) => string | undefined;
|
||||
className?: (props: { isActive: boolean; isPending: boolean }) => string | undefined;
|
||||
};
|
||||
export const NavLinkButton = ({
|
||||
to,
|
||||
className,
|
||||
...props
|
||||
}: NavLinkPropsType) => {
|
||||
export const NavLinkButton = ({ to, className, ...props }: NavLinkPropsType) => {
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
>
|
||||
<NavLink to={to} className={cn("group outline-none", props.fullWidth ? "w-full" : "")}>
|
||||
{({ isActive, isPending }) => (
|
||||
<ButtonContent
|
||||
className={className && className({ isActive, isPending })}
|
||||
{...props}
|
||||
/>
|
||||
<ButtonContent className={className && className({ isActive, isPending })} {...props} />
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
@@ -4,49 +4,52 @@ import {
|
||||
InformationCircleIcon,
|
||||
LightBulbIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BookOpenIcon,
|
||||
CheckCircleIcon,
|
||||
ChevronRightIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
const variantClasses = {
|
||||
export const variantClasses = {
|
||||
info: {
|
||||
className: "border-blue-400/20 bg-blue-400/30",
|
||||
icon: <InformationCircleIcon className="h-5 w-5 shrink-0 text-blue-400" />,
|
||||
text: "text-blue-200",
|
||||
textColor: "text-blue-200",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
warning: {
|
||||
className: "border-yellow-400/20 bg-yellow-400/30",
|
||||
icon: (
|
||||
<ExclamationCircleIcon className="h-5 w-5 shrink-0 text-yellow-400" />
|
||||
),
|
||||
text: "text-yellow-200",
|
||||
icon: <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-[hsl(347,77%,50%)]/30 bg-[hsl(347,77%,50%)]/20",
|
||||
icon: (
|
||||
<ExclamationTriangleIcon className="h-5 w-5 shrink-0 text-rose-400" />
|
||||
),
|
||||
text: "text-rose-200",
|
||||
className: "border-rose-500/20 bg-rose-500/30",
|
||||
icon: <ExclamationCircleIcon className="h-5 w-5 shrink-0 text-rose-400" />,
|
||||
textColor: "text-rose-200",
|
||||
linkClassName: "transition hover:bg-rose-500/40",
|
||||
},
|
||||
idea: {
|
||||
className: "border-green-400/20 bg-green-400/30",
|
||||
icon: <LightBulbIcon className="h-5 w-5 shrink-0 text-green-400" />,
|
||||
text: "text-green-200",
|
||||
textColor: "text-green-200",
|
||||
linkClassName: "transition hover:bg-green-400/40",
|
||||
},
|
||||
success: {
|
||||
className: "border-green-400/20 bg-green-400/30",
|
||||
icon: <CheckCircleIcon className="h-5 w-5 shrink-0 text-green-400" />,
|
||||
text: "text-green-200",
|
||||
textColor: "text-green-200",
|
||||
linkClassName: "transition hover:bg-green-400/40",
|
||||
},
|
||||
docs: {
|
||||
className:
|
||||
"border-blue-400/20 bg-blue-400/30 transition hover:bg-blue-400/40",
|
||||
icon: <BookOpenIcon className="h-5 w-5 shrink-0 text-blue-400" />,
|
||||
text: "text-blue-200",
|
||||
className: "border-blue-400/20 bg-blue-400/30",
|
||||
icon: <BookOpenIcon className="mt-0.5 h-5 w-5 shrink-0 text-blue-400" />,
|
||||
textColor: "text-blue-200",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -55,41 +58,69 @@ export function Callout({
|
||||
className,
|
||||
icon,
|
||||
variant,
|
||||
href,
|
||||
to,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
variant: keyof typeof variantClasses;
|
||||
href?: string;
|
||||
to?: string;
|
||||
}) {
|
||||
const variantDefinition = variantClasses[variant];
|
||||
|
||||
if (variant === "docs") {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.text}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
if (to !== undefined) {
|
||||
if (to.startsWith("http")) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
variantDefinition.linkClassName,
|
||||
className
|
||||
)}
|
||||
</div>
|
||||
<ArrowTopRightOnSquareIcon className="h-5 w-5 text-blue-400" />
|
||||
</a>
|
||||
);
|
||||
>
|
||||
<div className={"flex w-full items-start gap-x-2"}>
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.textColor}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
<ArrowTopRightOnSquareIcon className={cn("h-5 w-5", variantDefinition.textColor)} />
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={cn(
|
||||
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
variantDefinition.linkClassName,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={"flex w-full items-start gap-x-2"}>
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.textColor}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
<ChevronRightIcon className={cn("h-5 w-5", variantDefinition.textColor)} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -103,7 +134,7 @@ export function Callout({
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.text}>
|
||||
<Paragraph variant={"small"} className={variantDefinition.textColor}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
|
||||
@@ -82,9 +82,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(
|
||||
defaultChecked ?? false
|
||||
);
|
||||
const [isChecked, setIsChecked] = useState<boolean>(defaultChecked ?? false);
|
||||
const [isDisabled, setIsDisabled] = useState<boolean>(disabled ?? false);
|
||||
|
||||
const buttonClassName = variants[variant].button;
|
||||
@@ -157,10 +155,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
)}
|
||||
</div>
|
||||
{variant === "description" && (
|
||||
<Paragraph
|
||||
variant="small"
|
||||
className={cn("mt-0.5", descriptionClassName)}
|
||||
>
|
||||
<Paragraph variant="small" className={cn("mt-0.5", descriptionClassName)}>
|
||||
{description}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
@@ -12,10 +12,7 @@ const ClientTabsList = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center transition duration-100",
|
||||
className
|
||||
)}
|
||||
className={cn("inline-flex items-center justify-center transition duration-100", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -106,31 +106,19 @@ export function ClipboardField({
|
||||
const inputIcon = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(container, fullWidth ? "w-full" : "max-w-max", className)}
|
||||
>
|
||||
<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")}
|
||||
>
|
||||
{typeof icon === "string" ? (
|
||||
<NamedIcon name={icon} className={iconClassName} />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
</div>
|
||||
{typeof icon === "string" ? <NamedIcon name={icon} className={iconClassName} /> : icon}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
ref={inputIcon}
|
||||
value={
|
||||
isSecure
|
||||
? typeof secure === "string"
|
||||
? secure
|
||||
: "•".repeat(value.length)
|
||||
: value
|
||||
}
|
||||
value={isSecure ? (typeof secure === "string" ? secure : "•".repeat(value.length)) : value}
|
||||
readOnly={true}
|
||||
className={cn("shrink grow select-all overflow-x-auto", input)}
|
||||
size={value.length}
|
||||
@@ -146,13 +134,9 @@ export function ClipboardField({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
onClick={copy}
|
||||
className={cn("shrink-0 grow-0", button)}
|
||||
>
|
||||
<Button variant={buttonVariant} onClick={copy} className={cn("shrink-0 grow-0", button)}>
|
||||
{copied ? <CheckIcon className="h-4 w-4 text-green-500" /> : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +1,78 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useLocales } from "./LocaleProvider";
|
||||
|
||||
type DateTimeProps = {
|
||||
date: Date | string;
|
||||
timeZone?: string;
|
||||
className?: string;
|
||||
includeSeconds?: boolean;
|
||||
};
|
||||
|
||||
export const DateTime = ({ date, timeZone, className }: DateTimeProps) => {
|
||||
export const DateTime = ({ date, timeZone = "UTC", includeSeconds = true }: DateTimeProps) => {
|
||||
const locales = useLocales();
|
||||
|
||||
const realDate = typeof date === "string" ? new Date(date) : date;
|
||||
|
||||
const locales = useLocales();
|
||||
const isoString = realDate.toISOString();
|
||||
const formattedDate = new Intl.DateTimeFormat(locales, {
|
||||
const initialFormattedDateTime = formatDateTime(realDate, timeZone, locales, includeSeconds);
|
||||
|
||||
const [formattedDateTime, setFormattedDateTime] = useState<string>(initialFormattedDateTime);
|
||||
|
||||
useEffect(() => {
|
||||
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
|
||||
|
||||
setFormattedDateTime(
|
||||
formatDateTime(realDate, resolvedOptions.timeZone, locales, includeSeconds)
|
||||
);
|
||||
}, [locales, includeSeconds, realDate]);
|
||||
|
||||
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
|
||||
};
|
||||
|
||||
function formatDateTime(
|
||||
date: Date,
|
||||
timeZone: string,
|
||||
locales: string[],
|
||||
includeSeconds: boolean
|
||||
): string {
|
||||
return new Intl.DateTimeFormat(locales, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone,
|
||||
}).format(realDate);
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat(locales, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: includeSeconds ? "numeric" : undefined,
|
||||
timeZone,
|
||||
}).format(realDate);
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
return (
|
||||
<time dateTime={isoString} className={className}>
|
||||
{formattedDate} at {formattedTime}
|
||||
</time>
|
||||
);
|
||||
};
|
||||
export const DateTimeAccurate = ({ date, timeZone = "UTC" }: DateTimeProps) => {
|
||||
const locales = useLocales();
|
||||
|
||||
export const DateTimeAccurate = ({ date, timeZone }: DateTimeProps) => {
|
||||
const realDate = typeof date === "string" ? new Date(date) : date;
|
||||
|
||||
const locales = useLocales();
|
||||
const isoString = realDate.toISOString();
|
||||
const formattedDate = new Intl.DateTimeFormat(locales, {
|
||||
const initialFormattedDateTime = formatDateTimeAccurate(realDate, timeZone, locales);
|
||||
|
||||
const [formattedDateTime, setFormattedDateTime] = useState<string>(initialFormattedDateTime);
|
||||
|
||||
useEffect(() => {
|
||||
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
|
||||
|
||||
setFormattedDateTime(formatDateTimeAccurate(realDate, resolvedOptions.timeZone, locales));
|
||||
}, [locales, realDate]);
|
||||
|
||||
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
|
||||
};
|
||||
|
||||
function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]): string {
|
||||
const milliseconds = `00${date.getMilliseconds()}`.slice(-3);
|
||||
|
||||
const formattedDateTime = new Intl.DateTimeFormat(locales, {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
timeZone,
|
||||
}).format(realDate);
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat(locales, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
timeZone,
|
||||
}).format(realDate);
|
||||
}).format(date);
|
||||
|
||||
const milliseconds = `00${realDate.getMilliseconds()}`.slice(-3);
|
||||
|
||||
return (
|
||||
<time dateTime={isoString}>
|
||||
{formattedDate} {formattedTime}.{milliseconds}
|
||||
</time>
|
||||
);
|
||||
};
|
||||
return `${formatDateTime}.${milliseconds}`;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,7 @@ const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: DialogPrimitive.DialogPortalProps) => (
|
||||
const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{children}
|
||||
@@ -61,29 +57,14 @@ const DialogContent = React.forwardRef<
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -95,10 +76,7 @@ const DialogTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -7,7 +7,5 @@ export function Fieldset({
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-y-5", className)}>{children}</div>
|
||||
);
|
||||
return <div className={cn("flex flex-col gap-y-5", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,7 @@ import { Paragraph } from "./Paragraph";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export function FormError({
|
||||
children,
|
||||
id,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
}) {
|
||||
export function FormError({ children, id }: { children: React.ReactNode; id?: string }) {
|
||||
return (
|
||||
<>
|
||||
{children && (
|
||||
@@ -29,13 +23,7 @@ export function FormError({
|
||||
);
|
||||
}
|
||||
|
||||
export function ZodFormErrors({
|
||||
errors,
|
||||
path,
|
||||
}: {
|
||||
errors: z.ZodIssue[];
|
||||
path: string[];
|
||||
}) {
|
||||
export function ZodFormErrors({ errors, path }: { errors: z.ZodIssue[]; path: string[] }) {
|
||||
if (errors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -54,10 +54,7 @@ export default function FormSegmentedControl({
|
||||
<div className="text-sm">
|
||||
<RadioGroup.Label
|
||||
as="p"
|
||||
className={cn(
|
||||
"font-normal",
|
||||
checked ? "text-bright" : "text-dimmed"
|
||||
)}
|
||||
className={cn("font-normal", checked ? "text-bright" : "text-dimmed")}
|
||||
>
|
||||
{option.label}
|
||||
</RadioGroup.Label>
|
||||
|
||||
@@ -1,40 +1,90 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,11 +29,7 @@ function useHelp() {
|
||||
return React.useContext(HelpContext);
|
||||
}
|
||||
|
||||
export function Help({
|
||||
defaultOpen,
|
||||
allowDismissing = true,
|
||||
children,
|
||||
}: HelpProps) {
|
||||
export function Help({ defaultOpen, allowDismissing = true, children }: HelpProps) {
|
||||
const [open, setOpen] = React.useState(defaultOpen || false);
|
||||
|
||||
return (
|
||||
@@ -52,7 +48,7 @@ export function HelpTrigger({ title }: { title: string }) {
|
||||
<Button
|
||||
variant="tertiary/small"
|
||||
LeadingIcon="lightbulb"
|
||||
leadingIconClassName="text-slate-400"
|
||||
leadingIconClassName="text-amber-400"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import type { IconNames } from "./NamedIcon";
|
||||
import type { IconNamesOrString } from "./NamedIcon";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
|
||||
const variants = {
|
||||
@@ -44,24 +44,13 @@ const variants = {
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
variant?: keyof typeof variants;
|
||||
icon?: IconNames;
|
||||
icon?: IconNamesOrString;
|
||||
shortcut?: string;
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
type,
|
||||
shortcut,
|
||||
fullWidth = true,
|
||||
variant = "medium",
|
||||
icon,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
({ className, type, shortcut, fullWidth = true, variant = "medium", icon, ...props }, ref) => {
|
||||
const inputClassName = variants[variant].input;
|
||||
const iconClassName = variants[variant].iconSize;
|
||||
const iconOffsetClassName = variants[variant].iconOffset;
|
||||
@@ -70,25 +59,16 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<div className={cn("relative", fullWidth ? "w-full" : "max-w-max")}>
|
||||
{icon && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center">
|
||||
<NamedIcon
|
||||
name={icon}
|
||||
className={cn(iconClassName, "text-dimmed")}
|
||||
/>
|
||||
<NamedIcon name={icon} className={cn(iconClassName, "text-dimmed")} />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
inputClassName,
|
||||
icon ? iconOffsetClassName : "",
|
||||
className
|
||||
)}
|
||||
className={cn(inputClassName, icon ? iconOffsetClassName : "", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{shortcut && (
|
||||
<div className={cn(shortcutClassName, "absolute")}>{shortcut}</div>
|
||||
)}
|
||||
{shortcut && <div className={cn(shortcutClassName, "absolute")}>{shortcut}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,7 @@ type InputGroupProps = {
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
export function InputGroup({
|
||||
children,
|
||||
className,
|
||||
fullWidth,
|
||||
}: InputGroupProps) {
|
||||
export function InputGroup({ children, className, fullWidth }: InputGroupProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
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({
|
||||
className,
|
||||
children,
|
||||
variant = "medium",
|
||||
...props
|
||||
}: LabelProps) {
|
||||
return variant === "medium" ? (
|
||||
<Header3 className={cn(className)} {...props}>
|
||||
export function Label({ className, children, variant = "medium", ...props }: LabelProps) {
|
||||
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;
|
||||
@@ -52,11 +49,7 @@ export function LabelValueStack({
|
||||
side="bottom"
|
||||
button={
|
||||
<Paragraph variant={variation.value}>
|
||||
<a
|
||||
href={href}
|
||||
className="underline underline-offset-2"
|
||||
target="_blank"
|
||||
>
|
||||
<a href={href} className="underline underline-offset-2" target="_blank">
|
||||
{value}
|
||||
<ArrowTopRightOnSquareIcon className="ml-1 inline-block h-4 w-4 text-dimmed" />
|
||||
</a>
|
||||
|
||||
@@ -6,11 +6,7 @@ type Props = Parameters<typeof Link>[0] & {
|
||||
disabledClassName?: string;
|
||||
};
|
||||
|
||||
export function LinkDisabled({
|
||||
disabled = false,
|
||||
disabledClassName,
|
||||
...props
|
||||
}: Props) {
|
||||
export function LinkDisabled({ disabled = false, disabledClassName, ...props }: Props) {
|
||||
if (disabled) {
|
||||
return (
|
||||
<span {...props} className={cn(props.className, disabledClassName)}>
|
||||
|
||||
@@ -13,10 +13,7 @@ type LocaleContextProviderProps = {
|
||||
|
||||
const Context = createContext<LocaleContext | null>(null);
|
||||
|
||||
export const LocaleContextProvider = ({
|
||||
locales,
|
||||
children,
|
||||
}: LocaleContextProviderProps) => {
|
||||
export const LocaleContextProvider = ({ locales, children }: LocaleContextProviderProps) => {
|
||||
const value = { locales };
|
||||
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {
|
||||
ExclamationCircleIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
StopIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowPathIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BeakerIcon,
|
||||
BellAlertIcon,
|
||||
BoltIcon,
|
||||
@@ -17,6 +15,7 @@ import {
|
||||
BuildingOffice2Icon,
|
||||
CalendarDaysIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
@@ -39,6 +38,7 @@ import {
|
||||
MagnifyingGlassIcon,
|
||||
PlusIcon,
|
||||
PlusSmallIcon,
|
||||
QrCodeIcon,
|
||||
SquaresPlusIcon,
|
||||
StarIcon,
|
||||
UserCircleIcon,
|
||||
@@ -46,119 +46,74 @@ 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)} />
|
||||
),
|
||||
"arrow-right": (className: string) => (
|
||||
<ArrowRightIcon className={cn("text-white", className)} />
|
||||
),
|
||||
"arrow-left": (className: string) => (
|
||||
<ArrowLeftIcon className={cn("text-white", className)} />
|
||||
),
|
||||
background: (className: string) => (
|
||||
<CloudIcon className={cn("text-sky-400", className)} />
|
||||
),
|
||||
beaker: (className: string) => (
|
||||
<BeakerIcon className={cn("text-purple-500", className)} />
|
||||
),
|
||||
billing: (className: string) => (
|
||||
<CreditCardIcon className={cn("text-teal-500", className)} />
|
||||
),
|
||||
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)} />,
|
||||
"arrow-left": (className: string) => <ArrowLeftIcon className={cn("text-white", className)} />,
|
||||
background: (className: string) => <CloudIcon className={cn("text-sky-400", className)} />,
|
||||
beaker: (className: string) => <BeakerIcon className={cn("text-purple-500", className)} />,
|
||||
billing: (className: string) => <CreditCardIcon className={cn("text-teal-500", className)} />,
|
||||
calendar: (className: string) => (
|
||||
<CalendarDaysIcon className={cn("text-purple-500", className)} />
|
||||
),
|
||||
check: (className: string) => (
|
||||
<CheckIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
check: (className: string) => <CheckIcon className={cn("text-dimmed", className)} />,
|
||||
"chevron-down": (className: string) => (
|
||||
<ChevronDownIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
"chevron-up": (className: string) => (
|
||||
<ChevronUpIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
"chevron-up": (className: string) => <ChevronUpIcon className={cn("text-dimmed", className)} />,
|
||||
"chevron-left": (className: string) => (
|
||||
<ChevronLeftIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
"chevron-right": (className: string) => (
|
||||
<ChevronRightIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
countdown: (className: string) => (
|
||||
<HourglassIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
clock: (className: string) => (
|
||||
<ClockIcon className={cn("text-cyan-500", className)} />
|
||||
),
|
||||
close: (className: string) => (
|
||||
<XMarkIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
countdown: (className: string) => <HourglassIcon className={cn("text-amber-400", className)} />,
|
||||
clock: (className: string) => <ClockIcon className={cn("text-cyan-500", className)} />,
|
||||
close: (className: string) => <XMarkIcon className={cn("text-dimmed", className)} />,
|
||||
"connection-alert": (className: string) => (
|
||||
<BellAlertIcon className={cn("text-rose-500", className)} />
|
||||
),
|
||||
docs: (className: string) => (
|
||||
<BookOpenIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
dynamic: (className: string) => (
|
||||
<DynamicTriggerIcon className={cn("text-cyan-500", className)} />
|
||||
),
|
||||
error: (className: string) => (
|
||||
<ExclamationCircleIcon className={cn("text-rose-500", className)} />
|
||||
<BellAlertIcon className={cn("text-amber-500", className)} />
|
||||
),
|
||||
docs: (className: string) => <BookOpenIcon className={cn("text-slate-400", className)} />,
|
||||
dynamic: (className: string) => <DynamicTriggerIcon className={cn("text-cyan-500", className)} />,
|
||||
error: (className: string) => <ErrorIcon className={cn("text-rose-500", className)} />,
|
||||
"external-link": (className: string) => (
|
||||
<ArrowTopRightOnSquareIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
flag: (className: string) => (
|
||||
<FlagIcon className={cn("text-sky-500", className)} />
|
||||
),
|
||||
folder: (className: string) => (
|
||||
<FolderIcon className={cn("text-indigo-600", className)} />
|
||||
),
|
||||
envelope: (className: string) => (
|
||||
<EnvelopeIcon className={cn("text-cyan-500", className)} />
|
||||
),
|
||||
environment: (className: string) => (
|
||||
<KeyIcon className={cn("text-yellow-500", className)} />
|
||||
),
|
||||
globe: (className: string) => (
|
||||
<GlobeAltIcon className={cn("text-fuchsia-600", className)} />
|
||||
),
|
||||
flag: (className: string) => <FlagIcon className={cn("text-sky-500", className)} />,
|
||||
folder: (className: string) => <FolderIcon className={cn("text-indigo-600", className)} />,
|
||||
envelope: (className: string) => <EnvelopeIcon className={cn("text-cyan-500", className)} />,
|
||||
environment: (className: string) => <KeyIcon className={cn("text-yellow-500", className)} />,
|
||||
globe: (className: string) => <GlobeAltIcon className={cn("text-fuchsia-600", className)} />,
|
||||
"hand-raised": (className: string) => (
|
||||
<HandRaisedIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
heart: (className: string) => (
|
||||
<HeartIcon className={cn("text-rose-500", className)} />
|
||||
),
|
||||
id: (className: string) => (
|
||||
<FingerPrintIcon className={cn("text-rose-200", className)} />
|
||||
),
|
||||
info: (className: string) => (
|
||||
<InformationCircleIcon className={cn("text-blue-500", className)} />
|
||||
),
|
||||
heart: (className: string) => <HeartIcon className={cn("text-rose-500", className)} />,
|
||||
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)} />,
|
||||
integration: (className: string) => (
|
||||
<SquaresPlusIcon className={cn("text-teal-500", className)} />
|
||||
),
|
||||
"invite-member": (className: string) => (
|
||||
<UserPlusIcon className={cn("text-indigo-500", className)} />
|
||||
),
|
||||
job: (className: string) => (
|
||||
<WrenchScrewdriverIcon className={cn("text-teal-500", className)} />
|
||||
),
|
||||
key: (className: string) => (
|
||||
<KeyIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
lightbulb: (className: string) => (
|
||||
<LightBulbIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
job: (className: string) => <WrenchScrewdriverIcon className={cn("text-teal-500", className)} />,
|
||||
key: (className: string) => <KeyIcon className={cn("text-amber-400", className)} />,
|
||||
lightbulb: (className: string) => <LightBulbIcon className={cn("text-amber-400", className)} />,
|
||||
log: (className: string) => (
|
||||
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
@@ -166,42 +121,22 @@ const icons = {
|
||||
organization: (className: string) => (
|
||||
<BuildingOffice2Icon className={cn("text-fuchsia-600", className)} />
|
||||
),
|
||||
plus: (className: string) => (
|
||||
<PlusIcon className={cn("text-green-600", className)} />
|
||||
),
|
||||
plus: (className: string) => <PlusIcon className={cn("text-green-600", className)} />,
|
||||
"plus-small": (className: string) => (
|
||||
<PlusSmallIcon className={cn("text-green-600", className)} />
|
||||
),
|
||||
property: (className: string) => (
|
||||
<Cog8ToothIcon className={cn("text-slate-600", className)} />
|
||||
),
|
||||
refresh: (className: string) => (
|
||||
<ArrowPathIcon className={cn("text-bright", className)} />
|
||||
),
|
||||
search: (className: string) => (
|
||||
<MagnifyingGlassIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
settings: (className: string) => (
|
||||
<Cog8ToothIcon className={cn("text-slate-600", className)} />
|
||||
),
|
||||
spinner: (className: string) => (
|
||||
<Spinner className={className} color="blue" />
|
||||
),
|
||||
"spinner-white": (className: string) => (
|
||||
<Spinner className={className} color="white" />
|
||||
),
|
||||
star: (className: string) => (
|
||||
<StarIcon className={cn("text-yellow-500", className)} />
|
||||
),
|
||||
team: (className: string) => (
|
||||
<UserGroupIcon className={cn("text-blue-500", className)} />
|
||||
),
|
||||
trigger: (className: string) => (
|
||||
<BoltIcon className={cn("text-orange-500", className)} />
|
||||
),
|
||||
user: (className: string) => (
|
||||
<UserIcon className={cn("text-blue-600", className)} />
|
||||
),
|
||||
property: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
|
||||
"qr-code": (className: string) => <QrCodeIcon className={cn("text-amber-400", className)} />,
|
||||
refresh: (className: string) => <ArrowPathIcon className={cn("text-bright", className)} />,
|
||||
search: (className: string) => <MagnifyingGlassIcon className={cn("text-dimmed", className)} />,
|
||||
settings: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
|
||||
spinner: (className: string) => <Spinner className={className} color="blue" />,
|
||||
"spinner-white": (className: string) => <Spinner className={className} color="white" />,
|
||||
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)} />,
|
||||
trigger: (className: string) => <BoltIcon className={cn("text-orange-500", className)} />,
|
||||
user: (className: string) => <UserIcon className={cn("text-blue-600", className)} />,
|
||||
warning: (className: string) => (
|
||||
<ExclamationTriangleIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
@@ -209,8 +144,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)} />
|
||||
@@ -218,12 +156,11 @@ const icons = {
|
||||
"schedule-dynamic": (className: string) => (
|
||||
<ScheduleIcon className={cn("text-sky-500", className)} />
|
||||
),
|
||||
webhook: (className: string) => (
|
||||
<WebhookIcon className={cn("text-pink-500", className)} />
|
||||
),
|
||||
webhook: (className: string) => <WebhookIcon className={cn("text-pink-500", className)} />,
|
||||
};
|
||||
|
||||
export type IconNames = keyof typeof icons;
|
||||
export type IconNamesOrString = IconNames | (string & {});
|
||||
export const iconNames = Object.keys(icons) as IconNames[];
|
||||
|
||||
export function NamedIcon({
|
||||
@@ -231,7 +168,7 @@ export function NamedIcon({
|
||||
className,
|
||||
fallback,
|
||||
}: {
|
||||
name: string;
|
||||
name: IconNamesOrString;
|
||||
className: string;
|
||||
fallback?: JSX.Element;
|
||||
}) {
|
||||
@@ -241,12 +178,7 @@ export function NamedIcon({
|
||||
|
||||
if (hasIcon(name)) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid aspect-square min-h-fit place-items-center",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cn("grid aspect-square min-h-fit place-items-center", className)}>
|
||||
<CompanyIcon
|
||||
name={name}
|
||||
className={"h-full w-full p-[7%]"}
|
||||
@@ -255,7 +187,7 @@ export function NamedIcon({
|
||||
shapeRendering: "geometricPrecision",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -273,10 +205,12 @@ export function NamedIconInBox({
|
||||
name,
|
||||
className,
|
||||
fallback,
|
||||
iconClassName,
|
||||
}: {
|
||||
name: string;
|
||||
className?: string;
|
||||
fallback?: JSX.Element;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -285,7 +219,7 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ export const OperatingSystemContextProvider = ({
|
||||
};
|
||||
|
||||
const throwIfNoProvider = () => {
|
||||
throw new Error(
|
||||
"Please wrap your application in an OperatingSystemContextProvider."
|
||||
);
|
||||
throw new Error("Please wrap your application in an OperatingSystemContextProvider.");
|
||||
};
|
||||
|
||||
export const useOperatingSystem = () => {
|
||||
|
||||
@@ -11,17 +11,9 @@ type WithChildren = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function PageHeader({
|
||||
children,
|
||||
hideBorder,
|
||||
}: WithChildren & { hideBorder?: boolean }) {
|
||||
export function PageHeader({ children, hideBorder }: WithChildren & { hideBorder?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-4 pt-4",
|
||||
hideBorder ? "" : "border-b border-slate-800 pb-4"
|
||||
)}
|
||||
>
|
||||
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-slate-800 pb-4")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -49,10 +41,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
|
||||
className="flex items-center gap-1 text-slate-400 transition group-hover:text-white"
|
||||
>
|
||||
<ChevronLeftIcon className="h-6" />
|
||||
<Header1
|
||||
variant="dimmed"
|
||||
className="transition group-hover:text-white"
|
||||
>
|
||||
<Header1 textColor="dimmed" className="transition group-hover:text-white">
|
||||
{backButton.text}
|
||||
</Header1>
|
||||
</Link>
|
||||
@@ -107,24 +96,13 @@ export function PageInfoProperty({
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{icon && typeof icon === "string" ? (
|
||||
<NamedIcon name={icon} className="h-4 w-4" />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
{icon && typeof icon === "string" ? <NamedIcon name={icon} className="h-4 w-4" /> : icon}
|
||||
{label && (
|
||||
<Paragraph
|
||||
variant="extra-small/caps"
|
||||
className="mt-0.5 whitespace-nowrap"
|
||||
>
|
||||
<Paragraph variant="extra-small/caps" className="mt-0.5 whitespace-nowrap">
|
||||
{label}:
|
||||
</Paragraph>
|
||||
)}
|
||||
{typeof value === "string" || typeof value === "number" ? (
|
||||
<Paragraph variant="small">{value}</Paragraph>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
<Paragraph variant="small">{value}</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,11 +43,8 @@ export function PaginationControls({
|
||||
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-400">
|
||||
Showing{" "}
|
||||
<span className="font-medium">
|
||||
{(currentPage - 1) * pageSize + 1}
|
||||
</span>{" "}
|
||||
to <span className="font-medium">{currentPage * pageSize}</span> of{" "}
|
||||
Showing <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> to{" "}
|
||||
<span className="font-medium">{currentPage * pageSize}</span> of{" "}
|
||||
<span className="font-medium">{totalResults}</span> results
|
||||
</p>
|
||||
</div>
|
||||
@@ -86,10 +83,7 @@ export function PaginationControls({
|
||||
);
|
||||
}
|
||||
|
||||
function pageUrl(
|
||||
location: ReturnType<typeof useLocation>,
|
||||
page: number
|
||||
): string {
|
||||
function pageUrl(location: ReturnType<typeof useLocation>, page: number): string {
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
search.set("page", String(page));
|
||||
@@ -101,8 +95,7 @@ const baseClass =
|
||||
"relative inline-flex items-center border px-3.5 py-2 text-xs font-medium focus:z-20 transition";
|
||||
const unselectedClass =
|
||||
"bg-slate-700/20 border-slate-500 text-slate-400 hover:bg-slate-400 hover:text-slate-900";
|
||||
const selectedClass =
|
||||
"z-10 bg-slate-500 border-slate-500 hover:bg-slate-400 text-slate-900";
|
||||
const selectedClass = "z-10 bg-slate-500 border-slate-500 hover:bg-slate-400 text-slate-900";
|
||||
|
||||
function PageLinkComponent({
|
||||
page,
|
||||
@@ -114,19 +107,13 @@ function PageLinkComponent({
|
||||
if (page.type === "specific") {
|
||||
if (page.isCurrent) {
|
||||
return (
|
||||
<Link
|
||||
to={pageUrl(location, page.page)}
|
||||
className={cn(baseClass, selectedClass)}
|
||||
>
|
||||
<Link to={pageUrl(location, page.page)} className={cn(baseClass, selectedClass)}>
|
||||
{page.page}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
to={pageUrl(location, page.page)}
|
||||
className={cn(baseClass, unselectedClass)}
|
||||
>
|
||||
<Link to={pageUrl(location, page.page)} className={cn(baseClass, unselectedClass)}>
|
||||
{page.page}
|
||||
</Link>
|
||||
);
|
||||
@@ -154,10 +141,7 @@ type SpecificPageLink = {
|
||||
|
||||
// If there are less than or equal to 6 pages, just show all the pages.
|
||||
// If there are more than 5 pages, show the first 3, the current page, and the last 3.
|
||||
function calculatePageLinks(
|
||||
currentPage: number,
|
||||
totalPages: number
|
||||
): Array<PageLink> {
|
||||
function calculatePageLinks(currentPage: number, totalPages: number): Array<PageLink> {
|
||||
const pageLinks: Array<PageLink> = [];
|
||||
|
||||
if (totalPages <= 10) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user