Compare commits
1202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9947383f5a | |||
| 46e1a1493b | |||
| b1cdbf52d6 | |||
| 1491d4251d | |||
| 62ac5dd137 | |||
| 23c93a5712 | |||
| 208907e10e | |||
| 3ef588e4e5 | |||
| 10836735be | |||
| 090bc2e7d2 | |||
| 4fe2fd63de | |||
| 145fb24e4d | |||
| 2382d4a5c5 | |||
| adbcfe6f20 | |||
| 0754e99507 | |||
| dfb0a79abc | |||
| c808e40f61 | |||
| a9bb53b529 | |||
| 2636d9091e | |||
| 878da3c01f | |||
| 588461188f | |||
| d4145de9a7 | |||
| 188c4b0e24 | |||
| ecd050bece | |||
| b24aeea592 | |||
| 30ba73c4f1 | |||
| bf6a2a0319 | |||
| 4fc5de0dac | |||
| 11ff63a9d2 | |||
| 0d1bdac8ab | |||
| 9f076631ae | |||
| a0d663c0fb | |||
| 8fd68e30e9 | |||
| 5c13512091 | |||
| 1d7e5737a0 | |||
| 305e3b7ef2 | |||
| 03721cb18d | |||
| 773a6e2c81 | |||
| 2f13ac100f | |||
| a10782490f | |||
| 6ad91123f2 | |||
| 81f2d5e4ec | |||
| f249d8defa | |||
| 09aef5cda7 | |||
| 3028b6ad9d | |||
| 060b650845 | |||
| 6186a14398 | |||
| 916a353660 | |||
| 699878a5b1 | |||
| d2c9b64212 | |||
| 3102deccfb | |||
| 97c1b51332 | |||
| 4ab7082954 | |||
| 760f5de248 | |||
| de7e8c783e | |||
| 45b7af53e6 | |||
| aa7458fe37 | |||
| 1fd1d26780 | |||
| 6979eb6e66 | |||
| b0f926d28d | |||
| 82d8e7d3f3 | |||
| a214411218 | |||
| 2806cee7de | |||
| bd28556bf3 | |||
| 19a8d39fb0 | |||
| a907e2a988 | |||
| 403acac5ef | |||
| 708ebbde14 | |||
| 3ce5397072 | |||
| ee79ab30a9 | |||
| 2b21f5ba8a | |||
| 8c27236bc9 | |||
| b8d191052b | |||
| 979f5f88ae | |||
| fef1670533 | |||
| 3897e6e695 | |||
| fc083de102 | |||
| c7c5fd7fac | |||
| 4630960478 | |||
| 302bd02ff1 | |||
| b5db9f5e5c | |||
| 4b64c2d020 | |||
| 0c01e58db9 | |||
| 367ce2e412 | |||
| 92f80fd87c | |||
| 135cb4921f | |||
| 6cbc4f1190 | |||
| 1bf84548d7 | |||
| ce95744e98 | |||
| 0493cebaed | |||
| ac98219b76 | |||
| c8ddc19d5a | |||
| c6df574689 | |||
| 232b7cd26a | |||
| b9dc7ce89e | |||
| 8cf85443d5 | |||
| dd10717628 | |||
| 8bd1ca8b66 | |||
| 7f5ce165a0 | |||
| 7df1b90928 | |||
| 57a7e00bfa | |||
| 7362feed71 | |||
| b1b9321ad2 | |||
| a69f756e34 | |||
| 18a02829e3 | |||
| 0f6e580641 | |||
| 796f1209f2 | |||
| 4ce96b7d28 | |||
| b86ffa0d3d | |||
| 3ee7cd6ff6 | |||
| 74686c00cb | |||
| 8f3e550d03 | |||
| 65cff19ead | |||
| 591422b8cf | |||
| bbaa6ba156 | |||
| e20fa3c2ec | |||
| 57514f8771 | |||
| ef1a1ff343 | |||
| 2fbaa30b9f | |||
| 1199e934c2 | |||
| 83bcefe10c | |||
| 4ca9f182d7 | |||
| 5dec3ed4e6 | |||
| 955961eb5f | |||
| 33184a8130 | |||
| 5611768854 | |||
| a3278cb8a2 | |||
| d0e6442872 | |||
| 05b5ed6973 | |||
| 859425f093 | |||
| 1e374d30d0 | |||
| 9f649d2a27 | |||
| 0a20b6e05a | |||
| 78e2d0e9f9 | |||
| 24c8f08267 | |||
| 12de1b0d34 | |||
| b62df731fc | |||
| e347389b64 | |||
| 10f92a6124 | |||
| b957aa1752 | |||
| 3159619852 | |||
| 300c102b31 | |||
| 9e0b152c0e | |||
| a998d6c505 | |||
| 4e78da319c | |||
| 2c37091b08 | |||
| 5f739ee312 | |||
| fccba7db98 | |||
| 0f7b7ff6d6 | |||
| 603ffbc426 | |||
| beef7769c6 | |||
| e9088cf08c | |||
| 390bed0b9a | |||
| e3e3dab525 | |||
| b884a0ab53 | |||
| 871d6e1d52 | |||
| 7daa82fc7a | |||
| 9dd7fb2748 | |||
| c246578a81 | |||
| d395b95762 | |||
| f3bda59617 | |||
| ef8b52c20f | |||
| 2bedc779b9 | |||
| a5b5912270 | |||
| 275318d2df | |||
| 8d48ebdee8 | |||
| 7ef0fe79b5 | |||
| 921957958e | |||
| 73721fc55c | |||
| 4fcec3bd0d | |||
| 7f4b6b9974 | |||
| 950ab24e7f | |||
| c4da1b4a9a | |||
| 24debf0cee | |||
| 645643ea41 | |||
| 321378db4f | |||
| c8b10ce188 | |||
| 57ed2b2d0d | |||
| fa3a22eb78 | |||
| a1bc15d18b | |||
| 3f48dd0a68 | |||
| 5c71868a73 | |||
| 3ce6decef4 | |||
| ab007f2708 | |||
| 8412680863 | |||
| a90908df6e | |||
| a82b86210f | |||
| 0367a92731 | |||
| d230cf622f | |||
| a1bf073b43 | |||
| 5588108b97 | |||
| b9bcecffe3 | |||
| 59075f5fc2 | |||
| fa942fc9f5 | |||
| 47a8a557cf | |||
| b51d0b5399 | |||
| 763c84e3b3 | |||
| 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 | |||
| 155a4eecd4 | |||
| 5eec1b6f7b | |||
| e2ce9472cf | |||
| 06f0b69cfb | |||
| 0ae43fde72 | |||
| 32f5b7e5c4 | |||
| e3a804ca64 | |||
| 6a3ee289b3 | |||
| 02a8605fec | |||
| dbd72e6bbf | |||
| d629c95daf | |||
| 04d6e8e7f0 | |||
| 7caf95158d | |||
| 8245448f29 | |||
| 4d85bcd60e | |||
| d8d156adac | |||
| 7d02c84c47 | |||
| a5d70b8c37 | |||
| d42cd9ffac | |||
| 1b19c83998 | |||
| c0be8bef23 | |||
| 90bc2d0b60 | |||
| 1c747f69f5 | |||
| 57914c7488 | |||
| 1939222d31 | |||
| c29f5fab7a | |||
| adb2cda614 | |||
| c462441f9e | |||
| 2681f6897d | |||
| 2322c67aff | |||
| f645663fd8 | |||
| f2f4d4b827 | |||
| 85108cfcb5 | |||
| 308bd355e2 | |||
| 4ab032c717 | |||
| 17a117d1ee | |||
| bc43cc232d | |||
| 94ae9e65f4 | |||
| 2ed7158cf1 | |||
| 9d8a21a3de | |||
| d313eaa742 | |||
| c5d91e7cca | |||
| cc668a1624 | |||
| d17173ec0a | |||
| ebcd886d20 | |||
| 4976bd1889 | |||
| 9da6d92fb3 | |||
| 9dee5429d5 | |||
| ebe90aac5c | |||
| 289d69c79d | |||
| 5bdaf4f662 | |||
| 67f47bc3df | |||
| 67ddd76ff3 | |||
| 95de883879 | |||
| 3227da122e | |||
| ae243c783a | |||
| eb3b7f5146 | |||
| 414439cac9 | |||
| 7dacccb70e | |||
| a5e1d0595e | |||
| 7e8c6e92a2 | |||
| 24542d4e21 | |||
| b975125af5 | |||
| cc8c60605b | |||
| 23adc15ce3 | |||
| 7204aa0c9a | |||
| 6750429cd4 | |||
| cfe0a6cbb8 | |||
| a9f95971e8 | |||
| d7eee844b7 | |||
| 4c52731d16 | |||
| e53447d0bf | |||
| 53ed23ac58 | |||
| 226fb4a349 | |||
| d89632eb9d | |||
| 4621196993 | |||
| 988e753c74 | |||
| aa49b2ca76 | |||
| fdcd2fdb27 | |||
| 62f94206bb | |||
| 7b6647cfd4 | |||
| 73828c5db7 | |||
| e1d942bb3a | |||
| a764cf64b3 | |||
| 018d51993a | |||
| 4ce51291de | |||
| e0cd68787d | |||
| 6601f4c505 | |||
| 6f62f0e2f1 | |||
| c00026e203 | |||
| 38ac365953 | |||
| 7cfb0f7d2d | |||
| bae194f366 | |||
| 398d95528d | |||
| d1b2ee5490 | |||
| d39b9bc060 | |||
| 0c0b7d86ee | |||
| 03c4d9504f | |||
| 0737bfc5fe | |||
| 80b4916094 | |||
| 00845b43b4 | |||
| 83a74808d5 | |||
| 951679cc10 | |||
| 3cd95f06b2 | |||
| de00fa91ae | |||
| 25d4f0b347 | |||
| 413aa28594 | |||
| 29e153bc54 | |||
| 7b1a2f60d0 | |||
| 2302626703 | |||
| 28b3193743 | |||
| 0fdf8d0f6a | |||
| 890c93a820 | |||
| 3bfcde99dc | |||
| d5a8d19b66 | |||
| 7a3a9a6f7c | |||
| a816c52699 | |||
| bdd3157a45 | |||
| 7337e75d1e | |||
| b39f1694c3 | |||
| b3a919293c | |||
| 927eb1d0ba | |||
| b812c6d984 | |||
| 6a768c3f84 | |||
| 83f4e65bc3 | |||
| ad33abe6bc | |||
| 2e1624235c | |||
| 647f09f2f3 | |||
| d0ad2bd38b | |||
| 3ae4e11bbe | |||
| 6f19a3e750 | |||
| e8442eedad | |||
| a606db25f6 | |||
| 82f715bab6 | |||
| 28914b878c | |||
| 3c2f1709cc | |||
| 40ffde33b6 | |||
| 8184c21a80 | |||
| 5c13d0c2df | |||
| fd8fd22bdc | |||
| 31d8669c9d | |||
| 59e1fbf565 | |||
| 80c9bb0317 | |||
| 6c71bad8ef | |||
| 0e82f6287a | |||
| 52aacc1e1d | |||
| 909b4a5432 | |||
| b48b724c01 | |||
| 91ac1567d4 | |||
| c034035aba | |||
| cd852f9563 | |||
| 010ac6e5b6 | |||
| 0a3cf46068 | |||
| 2bbeeb860d | |||
| b74045fe55 | |||
| a940102d83 | |||
| 74e9d5eb3c | |||
| b256d66374 | |||
| 5b23e60b04 | |||
| 4cd71decbc | |||
| 1ba372e2c3 | |||
| d88f8c87f6 | |||
| ff02ab2aa0 | |||
| 38c71bdb3b | |||
| b68ce9695c | |||
| 0d3e7bd996 | |||
| 1e0acf4ce8 | |||
| b9e8e084a8 | |||
| 1701d4253d | |||
| ff60b71321 | |||
| f68439b936 | |||
| a3fe3944b7 | |||
| 9cf44777e1 | |||
| 5206c2c971 | |||
| 294fd10304 | |||
| 32650f8b9a | |||
| c5d96db9a8 | |||
| d04c66006b | |||
| d43529dce9 | |||
| 5ba95f3cbc | |||
| 7381e5b393 | |||
| b62f00dfd7 | |||
| 95787fcc66 | |||
| 4ef76f72dc | |||
| 939f33e791 | |||
| e15ade91d4 | |||
| 0776cc884d | |||
| 0ad09eb11f | |||
| 9e8b37c6cf | |||
| c2839934b4 | |||
| e2e3c3a9b3 | |||
| 9b3f4bc1a3 | |||
| cf9c34950f | |||
| d4a2927279 | |||
| 56172b1e5c | |||
| 7def5437fd | |||
| d21e364f11 | |||
| 7c6440fd9a | |||
| 0b61207ecf | |||
| fff2f40924 | |||
| 2166929fa8 | |||
| 5aa53da864 | |||
| 21d7ff2f74 | |||
| 8bba30e290 | |||
| fd94a32b95 | |||
| 25f9aa8c2d | |||
| 172bb4527c | |||
| 8ad769569c | |||
| 32eba22628 | |||
| 8d4179b5df | |||
| b7385825b6 | |||
| 5a20528cc8 | |||
| 324cf161ec | |||
| 9f84e77f57 | |||
| beaf0a9ecb | |||
| fb308e8038 | |||
| d2d15dae9f | |||
| 977f064157 | |||
| 8d23dd0186 | |||
| 90ae06df2a | |||
| 2cf253a914 | |||
| 9e462cc329 | |||
| 36ae8910b6 | |||
| fe0ed0bc9a | |||
| ec738515b7 | |||
| 287b6c6bc7 | |||
| aad93b9147 | |||
| 31176245bd | |||
| da45c7e2b9 | |||
| 3c73cb3fed | |||
| 275d01421e | |||
| 75edfccc31 | |||
| 1d230d9fed | |||
| 7d87dbf2ed | |||
| 44467331a2 | |||
| 29cbfd4fb3 | |||
| d946408105 | |||
| 6101372ba8 | |||
| 4993860a18 | |||
| 5e29119e83 | |||
| fe14664cfd | |||
| f871817a2f | |||
| 6ccacaa92e | |||
| 24e0f04aca | |||
| f878d499a7 | |||
| c24cd1c02b | |||
| c6028fca44 | |||
| 25daa5b358 | |||
| 0503a2a517 | |||
| 28fd3d9b18 | |||
| 1a9af1dee4 | |||
| ce27de1af6 | |||
| c9d5334a38 | |||
| bd20b7b8e0 | |||
| 52f40c8c26 | |||
| 622dd6aba4 | |||
| 969a6a1f2c | |||
| 08eab9622d | |||
| 107960b67c | |||
| b394f5e43f | |||
| 4d71a5c9d4 | |||
| 59e2216861 | |||
| 2fba5da04e | |||
| efbe2ddbf7 | |||
| 7f353723eb | |||
| a94d50f3cd | |||
| ae66e6f01a | |||
| e4b0b1e305 | |||
| 65ca2560fc | |||
| bdbf04efe9 | |||
| 4b56671d2d | |||
| f2b190b21a | |||
| 4f5cf931be | |||
| 68beefdaf2 | |||
| 4a52fd2edc | |||
| 47fdfb344d | |||
| ebbbc85d7b | |||
| 22ff2c36d9 | |||
| 5db2a1bb84 | |||
| 8a4ae0df20 | |||
| 80c3e48f5c | |||
| a7244032e4 | |||
| 7fc3ff26bc | |||
| 0648d230fc | |||
| 2626608200 | |||
| 081ef45047 | |||
| 9aa9ebc23f | |||
| f664a10024 | |||
| 52beb2f012 | |||
| 149e3c2d84 | |||
| 223965757c | |||
| 27c1d2516b | |||
| 014a97fcdc | |||
| eb85cc5c14 | |||
| b1d49d71ea | |||
| 9a0a6305e7 | |||
| a218bce73b | |||
| c94ca54aff | |||
| 2e8cd805fd | |||
| 1a9bb4ddc5 | |||
| e7f7c0704a | |||
| b5e0fb0e97 | |||
| f555989a5c | |||
| 2e0ea4520c | |||
| 31a9164df1 | |||
| b363a1e60d | |||
| e5714f9047 | |||
| e9b9c77edc | |||
| acd49994bd | |||
| 5264674cda | |||
| 2825925781 | |||
| 083bd7f1cf | |||
| 3f90fe04b4 | |||
| a63a247b22 | |||
| 7bb589b8f7 | |||
| 7c94ea3ec5 | |||
| 5789ae3c73 | |||
| 95fad3deb8 | |||
| 2b5b8d7931 | |||
| 96e0fb81ae | |||
| e5a568f510 | |||
| 8cb3155ea6 | |||
| 52c5d93449 | |||
| 5e3502f589 | |||
| f1fd36d3a1 | |||
| 9f49b7f4c0 | |||
| eca89e538b | |||
| bbac63e162 | |||
| ed3123470a | |||
| 170bda3158 | |||
| b5a8eec650 | |||
| 817b4ed133 | |||
| e4837cea9d | |||
| e1cdeb284f | |||
| f195933cb6 | |||
| 6e1d91b413 | |||
| d4e39de67e | |||
| 4903cfc689 | |||
| 54a8e26c7c | |||
| 4cc9006c8e | |||
| bc6492b1c4 | |||
| afd05f93fc | |||
| e83f5e7cca | |||
| 7b8e02023e | |||
| 3882f531cf | |||
| 0ce58b5c0f | |||
| f36a1c60f4 | |||
| 98fcc5d2d4 | |||
| 6ff2ee23f8 | |||
| e1405b140f | |||
| 04ab709ddd | |||
| 0736c68d77 | |||
| 34ccf345cd | |||
| 605a38d8ab | |||
| c9fd8ccae2 | |||
| 43482b1b96 | |||
| 81912b2530 | |||
| 2db2e7a369 | |||
| 3589d26fb3 | |||
| e5282fb264 | |||
| 61fddbb8af | |||
| e141aa3ff4 | |||
| 2fa1536315 | |||
| 5607dcfae9 | |||
| bd103802f9 | |||
| 0cbf427a58 | |||
| e53cd477f9 | |||
| df61f29a02 | |||
| feda01ab3b | |||
| fb2684c0b3 | |||
| 89e4b80e65 | |||
| d55fe2d100 | |||
| 49a510aaf9 | |||
| 16a5504a32 | |||
| b720c27c2a | |||
| cad3453e32 | |||
| afe0a8a159 | |||
| 2487e815a3 | |||
| 771348dd5e | |||
| 1f76e1228a | |||
| fe782e5ff8 | |||
| b4167a38f2 | |||
| 61459a197f | |||
| 30b6cc646a | |||
| df95d4e29f | |||
| 5090b64e57 | |||
| 12d188d85a | |||
| ab3e45d933 | |||
| 8c979cabd7 | |||
| a617922c1d | |||
| fec7cd65a7 | |||
| 10cfdb2fa2 | |||
| 43a62d8bec | |||
| 7b32b7ecd6 | |||
| 2fcd166d32 | |||
| d8c673157b | |||
| 99316df879 | |||
| 5a4fa3d2b3 | |||
| 71ececd7ce | |||
| 74519c08f0 | |||
| a85f19e016 | |||
| cfe887341d | |||
| eab17c3467 | |||
| 87c598627f | |||
| 30dc840144 | |||
| dd0e2d0bd2 |
+17
-8
@@ -1,18 +1,27 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"changelog": [
|
||||
"@remix-run/changelog-github",
|
||||
{
|
||||
"repo": "triggerdotdev/trigger.dev"
|
||||
}
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"fixed": [
|
||||
[
|
||||
"@trigger.dev/*"
|
||||
]
|
||||
],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"wss",
|
||||
"webapp",
|
||||
"integrations",
|
||||
"emails",
|
||||
"internal-cli",
|
||||
"integration-catalog"
|
||||
]
|
||||
}
|
||||
"@trigger.dev/database"
|
||||
],
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# You need to stop and re-run docker to switch, `pnpm run docker:db`
|
||||
# To login using GitHub and connect via OAuth with most integrations
|
||||
AUTH_CALLBACK_URL=http://localhost:3004/oauth/callback
|
||||
# To connect with Slack
|
||||
# AUTH_CALLBACK_URL=https://<ngrok subdomain>.eu.ngrok.io/oauth/callback
|
||||
|
||||
# You only need to use this locally if you specify PIZZLY_SECRET_KEY in the webapp .env file
|
||||
# PIZZLY_SECRET_KEY=<secret_key>
|
||||
+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
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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
|
||||
# This sets the URL used for direct connections to the database and should only be needed in limited circumstances
|
||||
# See: https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#fields:~:text=the%20shadow%20database.-,directUrl,-No
|
||||
DIRECT_URL=${DATABASE_URL}
|
||||
REMIX_APP_PORT=3030
|
||||
APP_ENV=development
|
||||
APP_ORIGIN=http://localhost:3030
|
||||
NODE_ENV=development
|
||||
|
||||
# OPTIONAL VARIABLES
|
||||
# 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=
|
||||
CLOUD_AIRTABLE_CLIENT_ID=
|
||||
CLOUD_AIRTABLE_CLIENT_SECRET=
|
||||
CLOUD_GITHUB_CLIENT_ID=
|
||||
CLOUD_GITHUB_CLIENT_SECRET=
|
||||
CLOUD_SLACK_APP_HOST=
|
||||
CLOUD_SLACK_CLIENT_ID=
|
||||
CLOUD_SLACK_CLIENT_SECRET=
|
||||
@@ -0,0 +1,226 @@
|
||||
name: 🚀 Publish Trigger.dev Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- improvements/*
|
||||
tags:
|
||||
- "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
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
env:
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: ʦ TypeScript
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck --filter webapp
|
||||
|
||||
unitTests:
|
||||
name: Unit Tests
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
pnpm run test
|
||||
|
||||
e2e:
|
||||
name: e2e Tests
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
# Setup environment variables
|
||||
cp ./.env.example ./.env
|
||||
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @examples/nextjs-test^...
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Execute tests
|
||||
pnpm run docker
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
pnpm run test:e2e
|
||||
|
||||
# Cleanup
|
||||
pnpm run docker:stop
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
publish:
|
||||
needs: [typecheck, unitTests, e2e]
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
short_sha: ${{ steps.get_commit.outputs.sha_short }}
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🆚 Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
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/improvements/* ]]; then
|
||||
ORIGINAL_VERSION="${GITHUB_REF#refs/heads/improvements/}"
|
||||
IMAGE_TAG="${ORIGINAL_VERSION}.rc"
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
elif [[ $GITHUB_REF == refs/heads/* ]]; then
|
||||
IMAGE_TAG="${GITHUB_REF#refs/heads/}"
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
else
|
||||
echo "Invalid reference: ${GITHUB_REF}"
|
||||
exit 1
|
||||
fi
|
||||
echo "::set-output name=version::${IMAGE_TAG}"
|
||||
- name: 🔢 Get the commit hash
|
||||
id: get_commit
|
||||
run: |
|
||||
echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7)
|
||||
|
||||
- name: 🐳 Build Docker Image
|
||||
run: |
|
||||
docker build -t release_build_image -f ./docker/Dockerfile .
|
||||
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐙 Push to GitHub Container Registry
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
- name: 🐙 Push 'latest' to GitHub Container Registry
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
|
||||
docker push $REGISTRY/$REPOSITORY:latest
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: trigger.dev
|
||||
@@ -0,0 +1,77 @@
|
||||
name: 🦋 Changesets Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/release.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- ".changeset/**"
|
||||
- "integrations/**"
|
||||
- "!integrations/**/*.md"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: 🦋 Changesets Release
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
if: |
|
||||
github.repository == 'triggerdotdev/trigger.dev'
|
||||
outputs:
|
||||
published_packages: ${{ steps.changesets.outputs.publishedPackages }}
|
||||
published: ${{ steps.changesets.outputs.published }}
|
||||
steps:
|
||||
- name: 🛑 Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: 🔐 Setup npm auth
|
||||
run: |
|
||||
echo "registry=https://registry.npmjs.org" >> ~/.npmrc
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc
|
||||
|
||||
# This action has two responsibilities. The first time the workflow runs
|
||||
# (initial push to the `main` branch) it will create a new branch and
|
||||
# then open a PR with the related changes for the new version. After the
|
||||
# PR is merged, the workflow will run again and this action will build +
|
||||
# publish to npm.
|
||||
- name: 🚀 PR / Publish
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm run changeset:version
|
||||
commit: "chore: Update version for release"
|
||||
title: "chore: Update version for release"
|
||||
publish: pnpm run changeset:release
|
||||
createGithubReleases: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
+6
-1
@@ -46,4 +46,9 @@ yarn-error.log*
|
||||
apps/**/public/build
|
||||
.tests-container-id.txt
|
||||
.sentryclirc
|
||||
.buildt
|
||||
.buildt
|
||||
|
||||
**/tmp/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"workspaceId":"63e5e42daf9a537ba8d9503c"}
|
||||
{ "workspaceId": "63e5e42daf9a537ba8d9503c" }
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
link-workspace-packages=false
|
||||
link-workspace-packages=false
|
||||
public-hoist-pattern[]=*prisma*
|
||||
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.env
|
||||
.env.local
|
||||
pnpm-lock.yaml
|
||||
tailwind.css
|
||||
.babelrc.json
|
||||
**/.react-email/
|
||||
**/storybook-static/
|
||||
**/.changeset/
|
||||
**/build/
|
||||
**/dist/
|
||||
Vendored
+6
-22
@@ -5,36 +5,20 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "pnpm run idev --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}"
|
||||
"name": "Debug WebApp",
|
||||
"command": "pnpm run dev --filter webapp",
|
||||
"envFile": "${workspaceFolder}/apps/webapp/.env",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Chrome webapp",
|
||||
"url": "http://localhost:3000",
|
||||
"url": "http://localhost:3030",
|
||||
"webRoot": "${workspaceFolder}/apps/webapp/app"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Current Test File",
|
||||
"autoAttachChildProcesses": true,
|
||||
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
|
||||
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
|
||||
"args": ["run", "${relativeFile}"],
|
||||
"smartStep": true,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
# Warp Launch Configuration
|
||||
#
|
||||
#
|
||||
# Use this to start a certain configuration of windows, tabs, and panes
|
||||
# Open the launch configuration palette to access and open any launch configuration
|
||||
#
|
||||
# This file defines your launch configuration
|
||||
# More on how to do so here:
|
||||
# https://docs.warp.dev/features/session-management/launch-configurations
|
||||
#
|
||||
# All launch configurations are stored under ~/.warp/launch_configurations/
|
||||
# Edit them anytime!
|
||||
#
|
||||
# You can also add commands that run on-start for your launch configurations like so:
|
||||
# ---
|
||||
# name: Example with Command
|
||||
# windows:
|
||||
# - tabs:
|
||||
# - layout:
|
||||
# cwd: /Users/warp-user/project
|
||||
# commands:
|
||||
# - exec: code .
|
||||
|
||||
---
|
||||
name: Triggerdotdev
|
||||
windows:
|
||||
- tabs:
|
||||
- title: webapp
|
||||
layout:
|
||||
split_direction: vertical
|
||||
panes:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: pnpm run build --filter webapp
|
||||
- exec: pnpm run i-dev --filter webapp
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/webapp
|
||||
- title: integrations
|
||||
layout:
|
||||
split_direction: vertical
|
||||
panes:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: pnpm run dev --filter integrations
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/integrations
|
||||
- title: ngrok
|
||||
layout:
|
||||
split_direction: horizontal
|
||||
panes:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: ./scripts/proxy-webapp.sh <your-trigger-dev>
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: ./scripts/proxy-pizzly.sh <your-pizzly-dev>
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: ./scripts/proxy-wss.sh <your-wss-dev>
|
||||
- title: wss and examples
|
||||
layout:
|
||||
split_direction: horizontal
|
||||
panes:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/wss
|
||||
commands:
|
||||
- exec: LOG_LEVEL=debug pnpm run dev
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/examples
|
||||
- title: emails
|
||||
layout:
|
||||
split_direction: horizontal
|
||||
panes:
|
||||
- cwd: /Users/eric/Work/Git/APIHero/trigger.dev/packages/emails
|
||||
commands:
|
||||
- exec: pnpm run dev
|
||||
- cwd: /Users/eric/Work/Git/APIHero/trigger.dev/packages/emails
|
||||
+9
-10
@@ -4,23 +4,22 @@ Trigger.dev uses [changesets](https://github.com/changesets/changesets) to manag
|
||||
|
||||
## Adding a changeset
|
||||
|
||||
To add a changeset, use `pnpm run changeset:add` and follow the instructions [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please only ever select one of our public packages when adding a changeset, which currently are:
|
||||
To add a changeset, use `pnpm run changeset:add` and follow the instructions [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please only ever select one of our public packages when adding a changeset.
|
||||
|
||||
- `@trigger.dev/sdk`
|
||||
- `@trigger.dev/integration-sdk`
|
||||
- `@trigger.dev/github`
|
||||
- `@trigger.dev/notion`
|
||||
- `@trigger.dev/slack`
|
||||
- `@trigger.dev/shopify`
|
||||
- `@trigger.dev/resend`
|
||||
|
||||
## Release instructions
|
||||
## Release instructions (local only)
|
||||
|
||||
Based on the instructions [here](https://github.com/changesets/changesets/blob/main/docs/intro-to-using-changesets.md)
|
||||
|
||||
1. Run `pnpm run changeset:version`
|
||||
2. Run `pnpm run changeset:release`
|
||||
|
||||
## Release instructions (CI)
|
||||
|
||||
Please follow the best-practice of adding changesets in the same commit as the code making the change with `pnpm run changeset:add`, as it will allow our release.yml CI workflow to function properly:
|
||||
|
||||
- Anytime new changesets are added in a commit in the `main` branch, the [release.yml](./.github/workflows/release.yml) workflow will run and will automatically create/update a PR with a fresh run of `pnpm run changeset:version`.
|
||||
- When the version PR is merged into `main`, the release.yml workflow will automatically run `pnpm run changeset:release` to build and release packages to npm.
|
||||
|
||||
## Pre-release instructions
|
||||
|
||||
1. Add changesets as usual `pnpm run changeset:add`
|
||||
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
# 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://github.com/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` file
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
5. Open it 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
|
||||
cd ./examples
|
||||
pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir --import-alias "@/*"
|
||||
```
|
||||
|
||||
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
|
||||
{
|
||||
// other package.json properties
|
||||
"devDependencies": { "@trigger.dev/cli": "workspace:*" }
|
||||
}
|
||||
```
|
||||
|
||||
5. Back in the terminal, navigate into the example, and initialize the CLI. When prompted, select `self-hosted` and enter `localhost:3030` if you are testing against the local instance of Trigger.dev, or you can just use the Trigger.dev cloud. When asked for an API key, use the key you copied earlier.
|
||||
|
||||
```sh
|
||||
cd ./test-cli
|
||||
pnpm i
|
||||
pnpm exec trigger-cli init
|
||||
```
|
||||
|
||||
6. If you are just testing the `init` command, you can stop here. If you'd like to test the `dev` command, first start the Next.js app on port 3000:
|
||||
|
||||
```sh
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
7. Open a new terminal window, and then run the `dev` command like so:
|
||||
|
||||
```sh
|
||||
pnpm exec trigger-cli dev
|
||||
```
|
||||
|
||||
8. Please remember to delete the temporary project you created after you've tested the changes, and before you raise a PR.
|
||||
|
||||
## Running end-to-end webapp tests
|
||||
|
||||
To run the end-to-end tests, follow the steps below:
|
||||
|
||||
1. Set up environment variables (copy example envs into the correct place)
|
||||
|
||||
```sh
|
||||
cp ./.env.example ./.env
|
||||
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
|
||||
```
|
||||
|
||||
2. Set up dependencies
|
||||
|
||||
```sh
|
||||
# Build packages
|
||||
pnpm run build --filter @examples/nextjs-test^...
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Install playwrite browsers (ONE TIME ONLY)
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
3. Set up the database
|
||||
|
||||
```sh
|
||||
pnpm run docker
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
```
|
||||
|
||||
4. Run the end-to-end tests
|
||||
|
||||
```sh
|
||||
pnpm run test:e2e
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
The end-to-end tests use a `setup` and `teardown` script to seed the database with test data. If the test runner doesn't exit cleanly, then the database can be left in a state where the tests can't run because the `setup` script will try to create data that already exists. If this happens, you can manually delete the `users` and `organizations` from the database using prisma studio:
|
||||
|
||||
```sh
|
||||
# With the database running (i.e. pnpm run docker)
|
||||
pnpm run db:studio
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -1,25 +1,201 @@
|
||||
Copyright (c) 2022 API Hero Inc.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
- All content that resides under any "ee/" directory of this repository, if such directories exists, are licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Trigger.dev Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
1. Definitions.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2023] [Trigger.dev]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -4,18 +4,62 @@
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/3f5ad4c1-c4c8-4277-b622-290e7f37bd00/public">
|
||||
<img alt="Trigger.dev logo" src="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/a45d1fa2-0ae8-4a39-4409-f4f934bfae00/public">
|
||||
</picture>
|
||||
|
||||
### The background jobs framework for Next.js
|
||||
|
||||
[Discord](https://discord.gg/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>
|
||||
|
||||
# **The background jobs framework for Next.js**
|
||||
# About Trigger.dev
|
||||
|
||||
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
|
||||
|
||||
|
||||
### Long running Jobs on serverless
|
||||
|
||||
### _**Trigger.dev v2 is launching soon...**_
|
||||
Reliably run jobs and don’t worry about function timeouts, we handle those for you.
|
||||
|
||||
Visit the new landing page for [Trigger.dev v2](https://next.trigger.dev) and [sign up for early access.](https://bcymafitv0e.typeform.com/to/QQnotGJM#source=github)
|
||||
- Auto-resume after a function timeout
|
||||
- Auto-resume after a server outage
|
||||
- Add delays of up to a year
|
||||
|
||||
### In your codebase
|
||||
|
||||
Create Jobs where they belong: in your codebase. Version control, localhost, test, review, and deploy like you're already used to.
|
||||
|
||||
### Secure by design
|
||||
|
||||
We only receive Triggers and the data you choose to send to us. You can even completely self-host the entire platform.
|
||||
|
||||
### Don't worry about deployment
|
||||
|
||||
Just use our SDK to write Jobs in your Next.js codebase. There's nothing extra to deploy and no CI to configure, your Jobs just connect to our cloud. Or you can always self-host.
|
||||
|
||||
### Full visibility of every job run
|
||||
|
||||
View every Task in every Run so you can tell exactly what happened.
|
||||
|
||||

|
||||
|
||||
### Built-in integrations
|
||||
|
||||
Easily integrate with hundreds of third-party APIs – including your own. Use API keys (which never leave your server) or let us handle OAuth for you. Install our integration packages and easily subscribe to webhooks and perform common tasks, or you can easily use your existing favorite Node.JS SDKs and get resumability and idempotency through our `runTask` function.
|
||||
|
||||
### Trigger.dev Connect (coming soon)
|
||||
|
||||
Easily add integrations for your users.
|
||||
|
||||
# Getting started
|
||||
|
||||
Visit our docs [here](https://trigger.dev/docs).
|
||||
|
||||
## Self-host
|
||||
|
||||
We provide an official trigger.dev docker image you can use to easily self-host the platform. We're working on more extensive guides but we currently provide a [Fly.io example repository](https://github.com/triggerdotdev/fly.io) with instructions in the README for deploying and using a self-hosted instance of Trigger.dev on Fly.io.
|
||||
|
||||
## Development
|
||||
|
||||
To setup and develop locally or contribute to the open source project, follow our [development guide](./CONTRIBUTING.md).
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
TRIGGER_API_KEY=
|
||||
|
||||
TRIGGER_LOG_LEVEL=debug
|
||||
|
||||
|
||||
|
||||
##########################################################################################
|
||||
# ************************************* BASICS *******************************************
|
||||
##########################################################################################
|
||||
|
||||
APP_ENV=development
|
||||
|
||||
APP_ORIGIN=http://localhost:3000
|
||||
|
||||
NODE_ENV=development
|
||||
|
||||
PULSAR_ENABLED=1
|
||||
|
||||
SESSION_SECRET=
|
||||
|
||||
|
||||
|
||||
#########################################################################################
|
||||
# ************************************* LOGIN *******************************************
|
||||
#########################################################################################
|
||||
|
||||
GITHUB_CLIENT_ID=
|
||||
|
||||
GITHUB_SECRET=
|
||||
|
||||
LOGIN_ORIGIN=http://localhost:3000
|
||||
|
||||
MAGIC_LINK_SECRET=
|
||||
|
||||
|
||||
|
||||
############################################################################################
|
||||
# ************************************ OPTIONAL ********************************************
|
||||
############################################################################################
|
||||
|
||||
POSTHOG_PROJECT_KEY=
|
||||
|
||||
|
||||
|
||||
############################################################################################
|
||||
# ************************************ DATABASE ********************************************
|
||||
############################################################################################
|
||||
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
|
||||
|
||||
|
||||
|
||||
################################################################################################
|
||||
# ********************************** INTEGRATIONS **********************************************
|
||||
################################################################################################
|
||||
|
||||
PIZZLY_HOST=http://localhost:3004
|
||||
|
||||
PIZZLY_SECRET_KEY=
|
||||
|
||||
|
||||
|
||||
#########################################################################################
|
||||
# ************************************* EMAIL *******************************************
|
||||
#########################################################################################
|
||||
|
||||
FROM_EMAIL=
|
||||
|
||||
REPLY_TO_EMAIL=
|
||||
|
||||
RESEND_API_KEY=<api_key>
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
{"workspaceId":"63e5e42daf9a537ba8d9503c"}
|
||||
@@ -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",
|
||||
|
||||
@@ -3,6 +3,7 @@ import "../app/tailwind.css";
|
||||
import { unstable_createRemixStub } from "@remix-run/testing";
|
||||
import React from "react";
|
||||
import { LocaleContextProvider } from "../app/components/primitives/LocaleProvider";
|
||||
import { OperatingSystemContextProvider } from "../app/components/primitives/OperatingSystemProvider";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
@@ -33,9 +34,11 @@ const preview: Preview = {
|
||||
]);
|
||||
|
||||
return (
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixStub initialEntries={["/"]} />
|
||||
</LocaleContextProvider>
|
||||
<OperatingSystemContextProvider platform="mac">
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixStub initialEntries={["/"]} />
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
FROM node:16-bullseye AS pruner
|
||||
RUN apt-get update && apt-get install openssl -y
|
||||
WORKDIR /app
|
||||
RUN npm install turbo@1.7.0 -g
|
||||
COPY . .
|
||||
RUN turbo prune --scope=webapp --docker
|
||||
RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
|
||||
|
||||
# Base strategy to have layer caching
|
||||
FROM node:16-bullseye AS base
|
||||
RUN apt-get update && apt-get install openssl ca-certificates g++ make wget python3 -y
|
||||
ENV PULSAR_CPP_CLIENT_VERSION=2.10.3
|
||||
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client.deb -q
|
||||
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client-dev.deb -q
|
||||
RUN dpkg -i ./apache-pulsar-client*.deb
|
||||
WORKDIR /app
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||
|
||||
FROM base AS production-deps
|
||||
WORKDIR /app
|
||||
RUN npm install turbo@1.7.0 -g
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV production
|
||||
RUN npm config set python /usr/bin/python3
|
||||
COPY --from=pruner /app/out/full/apps/webapp/prisma/schema.prisma /app/apps/webapp/prisma/schema.prisma
|
||||
RUN pnpm install --prod --frozen-lockfile
|
||||
RUN pnpx prisma generate --schema /app/apps/webapp/prisma/schema.prisma
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
RUN npm install turbo@1.7.0 -g
|
||||
COPY turbo.json turbo.json
|
||||
RUN corepack enable
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
RUN npm config set python /usr/bin/python3
|
||||
ENV NODE_ENV development
|
||||
RUN pnpm install --ignore-scripts --frozen-lockfile
|
||||
ENV NODE_ENV production
|
||||
RUN pnpm run generate
|
||||
RUN pnpm run build --filter=webapp...
|
||||
RUN pnpx prisma migrate deploy --schema apps/webapp/prisma/schema.prisma
|
||||
RUN pnpm run db:seed --filter=webapp
|
||||
|
||||
# Runner
|
||||
FROM node:16-bullseye AS runner
|
||||
RUN apt-get update && apt-get install openssl ca-certificates g++ make wget python3 -y
|
||||
ENV PULSAR_CPP_CLIENT_VERSION=2.10.3
|
||||
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client.deb -q
|
||||
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client-dev.deb -q
|
||||
RUN dpkg -i ./apache-pulsar-client*.deb
|
||||
RUN npm install turbo -g
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV production
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 remixjs
|
||||
RUN chown -R remixjs:nodejs /app
|
||||
USER remixjs
|
||||
|
||||
COPY --from=pruner --chown=remixjs:nodejs /app/out/full/ .
|
||||
COPY --from=production-deps --chown=remixjs:nodejs /app .
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/app/styles/tailwind.css ./apps/webapp/app/styles/tailwind.css
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/build/server.js ./apps/webapp/build/server.js
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/build ./apps/webapp/build
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/public ./apps/webapp/public
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/prisma/schema.prisma ./apps/webapp/build/schema.prisma
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/prisma/migrations ./apps/webapp/build/migrations
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/node_modules/.prisma/client/libquery_engine-debian-openssl-1.1.x.so.node ./apps/webapp/build/libquery_engine-debian-openssl-1.1.x.so.node
|
||||
|
||||
# release_command = "pnpx prisma migrate deploy --schema apps/webapp/prisma/schema.prisma"
|
||||
ENTRYPOINT ["pnpm", "--filter", "webapp", "run", "start"]
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ApiEventLog } from "@trigger.dev/core";
|
||||
import { EventRecord } from "@trigger.dev/database";
|
||||
|
||||
export function eventRecordToApiJson(eventRecord: EventRecord): ApiEventLog {
|
||||
return {
|
||||
id: eventRecord.eventId,
|
||||
name: eventRecord.name,
|
||||
payload: eventRecord.payload as any,
|
||||
context: eventRecord.context as any,
|
||||
timestamp: eventRecord.timestamp,
|
||||
deliverAt: eventRecord.deliverAt,
|
||||
deliveredAt: eventRecord.deliveredAt,
|
||||
cancelledAt: eventRecord.cancelledAt,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function DynamicTriggerIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.0151 4C2.01629 2.89543 2.91269 2.00097 4.01726 2.00216L16.0172 2.0151C17.1218 2.01629 18.0163 2.91269 18.0151 4.01726L18.0021 16.0172C18.001 17.1218 17.1046 18.0163 16 18.0151L4 18.0021C2.89543 18.001 2.00097 17.1046 2.00216 16L2.0151 4ZM9.26429 4.75782C9.26473 4.34361 9.60088 4.00818 10.0151 4.00863C10.4293 4.00907 10.7647 4.34522 10.7643 4.75944L10.7606 8.1988L13.1952 5.76942C13.4884 5.47684 13.9633 5.47735 14.2559 5.77056C14.5484 6.06377 14.5479 6.53864 14.2547 6.83122L11.8201 9.26058L15.2594 9.26429C15.6736 9.26473 16.0091 9.60088 16.0086 10.0151C16.0082 10.4293 15.672 10.7647 15.2578 10.7643L11.8185 10.7606L14.2478 13.1952C14.5404 13.4884 14.5399 13.9633 14.2467 14.2558C13.9535 14.5484 13.4786 14.5479 13.186 14.2547L10.7567 11.8201L10.753 15.2594C10.7525 15.6736 10.4164 16.0091 10.0022 16.0086C9.58794 16.0082 9.25252 15.672 9.25296 15.2578L9.25667 11.8185L6.82209 14.2478C6.52888 14.5404 6.05401 14.5399 5.76143 14.2467C5.46885 13.9535 5.46937 13.4786 5.76258 13.186L8.19718 10.7567L4.75782 10.753C4.34361 10.7525 4.00818 10.4164 4.00863 10.0022C4.00907 9.58794 4.34522 9.25252 4.75944 9.25296L8.19877 9.25667L5.76942 6.82207C5.47684 6.52886 5.47735 6.05399 5.77056 5.76141C6.06377 5.46883 6.53864 5.46935 6.83122 5.76255L9.26058 8.19716L9.26429 4.75782Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
export function OneTreeIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_4914_58397)">
|
||||
<g filter="url(#filter0_d_4914_58397)">
|
||||
<path
|
||||
d="M9 14H11C11 14 11 15 11 17C11 19 11.5 20 11.5 20H8.5C8.5 20 9 19 9 17C9 15 9 14 9 14Z"
|
||||
fill="url(#paint0_linear_4914_58397)"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d_4914_58397)">
|
||||
<circle cx="5.5" cy="8.5" r="3.5" fill="currentColor" />
|
||||
</g>
|
||||
<g filter="url(#filter2_d_4914_58397)">
|
||||
<circle cx="8" cy="12" r="3" fill="currentColor" />
|
||||
</g>
|
||||
<circle cx="13" cy="11" r="4" fill="currentColor" />
|
||||
<circle cx="15.5" cy="7.5" r="2.5" fill="currentColor" />
|
||||
<g filter="url(#filter3_d_4914_58397)">
|
||||
<circle cx="9" cy="6" r="4" fill="currentColor" />
|
||||
</g>
|
||||
<circle cx="12.5" cy="5.5" r="3.5" fill="currentColor" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_4914_58397"
|
||||
x="4.5"
|
||||
y="14"
|
||||
width="11"
|
||||
height="14"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="4" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_4914_58397"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_d_4914_58397"
|
||||
x="-2"
|
||||
y="5"
|
||||
width="15"
|
||||
height="15"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="4" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_4914_58397"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_d_4914_58397"
|
||||
x="1"
|
||||
y="9"
|
||||
width="14"
|
||||
height="14"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="4" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_4914_58397"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter3_d_4914_58397"
|
||||
x="1"
|
||||
y="-2"
|
||||
width="16"
|
||||
height="16"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_4914_58397"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_4914_58397"
|
||||
x1="10"
|
||||
y1="14"
|
||||
x2="10"
|
||||
y2="20"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#402309" />
|
||||
<stop offset="1" stopColor="#713F12" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4914_58397">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
export function SaplingIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_4906_23816)">
|
||||
<path
|
||||
d="M4.39538 17.7403C7.25332 16.3563 13.2867 16.4291 15.5096 17.7403C16.6666 18.4227 17.2938 19.0069 17.6294 19.4103C17.8542 19.6806 17.651 20 17.2994 20H2.34215C2.151 20 1.9749 19.8835 2.01519 19.6967C2.09465 19.3281 2.53308 18.6421 4.39538 17.7403Z"
|
||||
fill="url(#paint0_linear_4906_23816)"
|
||||
/>
|
||||
<g filter="url(#filter0_d_4906_23816)">
|
||||
<path
|
||||
d="M9.37762 9.80872C7.43627 7.16364 3.49903 8.13783 2.43836 8.66818C2.26159 9.02174 3.32907 8.80396 4.08851 10.8279C4.84794 12.8519 7.63503 13.1864 8.74087 12.0806C9.27121 11.5502 9.69141 10.2363 9.37762 9.80872Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d_4906_23816)">
|
||||
<path
|
||||
d="M9.03577 9.94522L9.99862 9.80223C9.99862 9.80223 10.0387 16.2908 10.9485 16.8179C11.8582 17.345 8.47933 17.1309 8.92618 16.8179C9.37303 16.505 9.03577 9.94522 9.03577 9.94522Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter2_d_4906_23816)">
|
||||
<path
|
||||
d="M9.36235 10.1127C9.36235 5.78131 13.3229 4.41473 15.9395 4.00389C16.1892 3.96468 16.3818 4.22916 16.2961 4.46698C16.0763 5.07608 15.7914 6.09975 15.6717 7.46762C15.5019 9.40897 14.8971 10.3478 14.3613 10.962C13.3664 12.1026 9.36235 12.2341 9.36235 10.1127Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_4906_23816"
|
||||
x="-1.58107"
|
||||
y="8.04684"
|
||||
width="15.0664"
|
||||
height="12.6571"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="4" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_4906_23816"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_d_4906_23816"
|
||||
x="4.88525"
|
||||
y="9.80223"
|
||||
width="10.2195"
|
||||
height="15.3357"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="4" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_4906_23816"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_d_4906_23816"
|
||||
x="5.36235"
|
||||
y="4"
|
||||
width="14.9544"
|
||||
height="15.7756"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="4" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_4906_23816"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_4906_23816"
|
||||
x1="10"
|
||||
y1="16.7294"
|
||||
x2="10"
|
||||
y2="20"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.291667" stopColor="#402309" />
|
||||
<stop offset="1" stopColor="#713F12" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4906_23816">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export function TwoTreesIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_4906_58417)">
|
||||
<path
|
||||
d="M10 20V17"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.7239 16.3L14.8274 13H15.1171C15.3061 12.9973 15.4901 12.9371 15.6464 12.8271C15.8027 12.717 15.9243 12.5619 15.9963 12.3808C16.0683 12.1998 16.0873 12.001 16.0512 11.8088C16.015 11.6167 15.9252 11.4398 15.7929 11.3L12.8965 8H13.0896C13.2868 8.01843 13.4847 7.97361 13.6565 7.87162C13.8282 7.76963 13.9656 7.6154 14.0499 7.42984C14.1342 7.24428 14.1614 7.03636 14.1278 6.83425C14.0942 6.63215 14.0014 6.44564 13.862 6.3L10 2L7.29699 5.0096C8.25755 5.0792 9.16726 5.48607 9.85673 6.16512C10.5124 6.81087 10.9224 7.65741 11.023 8.56163C11.6295 8.91846 12.136 9.42887 12.4856 10.0473C12.9492 10.8671 13.1066 11.8229 12.9286 12.7472C12.7507 13.6714 12.2496 14.5014 11.5168 15.0945C10.8041 15.6712 9.91568 15.9888 8.99914 15.999V18H17.0481C17.237 17.9973 17.4211 17.9371 17.5774 17.8271C17.7337 17.717 17.8553 17.5618 17.9273 17.3808C17.9992 17.1998 18.0183 17.001 17.9822 16.8088C17.946 16.6167 17.8562 16.4398 17.7239 16.3ZM7.99914 18V15.9991H6.00231V18H7.99914ZM4.97036 15.9991H5.00231L5.00231 18H2.95193C2.76296 17.9973 2.57892 17.9371 2.42263 17.8271C2.26635 17.717 2.14468 17.5618 2.07272 17.3808C2.00076 17.1998 1.98168 17.001 2.01783 16.8088C2.05398 16.6167 2.14377 16.4398 2.27609 16.3L3.01417 15.4591C3.59927 15.795 4.26325 15.9821 4.94587 15.9988C4.95403 15.999 4.9622 15.9991 4.97036 15.9991ZM3.69171 14.6872C4.08702 14.8804 4.52307 14.9881 4.97036 14.9991H8.93032C9.64375 15.0045 10.3365 14.7632 10.8877 14.3171C11.4389 13.871 11.8137 13.2485 11.9466 12.5581C12.0796 11.8678 11.9623 11.1534 11.6151 10.5395C11.268 9.9256 10.7131 9.45119 10.0472 9.19898V8.99898C10.0472 8.20331 9.7263 7.44023 9.15504 6.87761C8.58378 6.31499 7.80899 5.99891 7.00111 5.99891C6.77881 5.99891 6.55901 6.02285 6.34528 6.06926L6.13804 6.3C5.99864 6.44564 5.90584 6.63215 5.87222 6.83425C5.8386 7.03636 5.86579 7.24428 5.9501 7.42984C6.03441 7.6154 6.17176 7.76963 6.34354 7.87162C6.51533 7.97361 6.71323 8.01843 6.91043 8H7.10353L4.20706 11.3C4.07475 11.4398 3.98496 11.6167 3.94881 11.8088C3.91266 12.001 3.93174 12.1998 4.0037 12.3808C4.07566 12.5619 4.19732 12.717 4.35361 12.8271C4.5099 12.9371 4.69394 12.9973 4.88291 13H5.17255L3.69171 14.6872Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7 20L7 13.9814"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.0472 9V9.2C10.7131 9.4522 11.268 9.92661 11.6151 10.5405C11.9623 11.1544 12.0796 11.8687 11.9466 12.5591C11.8137 13.2494 11.4389 13.8719 10.8877 14.318C10.3365 14.7641 9.64374 15.0054 8.93032 15H4.97036C4.26546 14.9827 3.58847 14.7251 3.05487 14.2712C2.52126 13.8172 2.1641 13.195 2.0443 12.5106C1.92449 11.8263 2.04946 11.1222 2.3979 10.5185C2.74634 9.91474 3.29666 9.44875 3.95499 9.2V9C3.95499 8.20435 4.27592 7.44129 4.84718 6.87868C5.41843 6.31607 6.19323 6 7.00111 6C7.80899 6 8.58378 6.31607 9.15504 6.87868C9.7263 7.44129 10.0472 8.20435 10.0472 9Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4906_58417">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</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"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
@@ -0,0 +1,57 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const variant = {
|
||||
small:
|
||||
"py-[0.25rem] px-1.5 text-xxs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-[0.125rem]",
|
||||
normal:
|
||||
"py-1 px-1.5 text-xs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-sm",
|
||||
};
|
||||
|
||||
type ActiveBadgeProps = {
|
||||
active: boolean;
|
||||
className?: string;
|
||||
badgeSize?: keyof typeof variant;
|
||||
};
|
||||
|
||||
export function ActiveBadge({ active, className, badgeSize = "normal" }: ActiveBadgeProps) {
|
||||
switch (active) {
|
||||
case true:
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-slate-800 text-green-500", className)}>
|
||||
Active
|
||||
</span>
|
||||
);
|
||||
case false:
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-slate-800 text-dimmed", className)}>
|
||||
Disabled
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function MissingIntegrationBadge({
|
||||
className,
|
||||
badgeSize = "normal",
|
||||
}: {
|
||||
className?: string;
|
||||
badgeSize?: keyof typeof variant;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-rose-600 text-white", className)}>
|
||||
Missing Integration
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewBadge({
|
||||
className,
|
||||
badgeSize = "normal",
|
||||
}: {
|
||||
className?: string;
|
||||
badgeSize?: keyof typeof variant;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-green-600 text-background", className)}>New!</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ReactNode } from "react";
|
||||
import { MainCenteredContainer } from "./layout/AppLayout";
|
||||
import { Header2 } from "./primitives/Headers";
|
||||
import { NamedIconInBox } from "./primitives/NamedIcon";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
|
||||
type ComingSoonProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
export function ComingSoon({ title, description, icon }: ComingSoonProps) {
|
||||
return (
|
||||
<MainCenteredContainer>
|
||||
<div className="flex flex-col justify-center rounded border border-slate-800 bg-slate-850">
|
||||
<div className="flex items-center gap-2 border-b border-slate-750 px-4 py-4">
|
||||
{typeof icon === "string" ? (
|
||||
<NamedIconInBox name={icon} className={"h-10 w-10 bg-midnight-800"} />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
<div className="mt-0.5 flex flex-col gap-y-1">
|
||||
<Paragraph variant="extra-extra-small/caps">Coming soon</Paragraph>
|
||||
<Header2 className="-mt-0.5">{title}</Header2>
|
||||
</div>
|
||||
</div>
|
||||
<Paragraph variant="small" className="p-4">
|
||||
{description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</MainCenteredContainer>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
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";
|
||||
import { FormError } from "./primitives/FormError";
|
||||
import { InputGroup } from "./primitives/InputGroup";
|
||||
import { Label } from "./primitives/Label";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./primitives/Select";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "./primitives/Sheet";
|
||||
import { TextArea } from "./primitives/TextArea";
|
||||
import { DiscordIcon } from "@trigger.dev/companyicons";
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
type FeedbackProps = {
|
||||
button: ReactNode;
|
||||
defaultValue?: FeedbackType;
|
||||
};
|
||||
|
||||
export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const lastSubmission = useActionData();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [form, { path, feedbackType, message }] = useForm({
|
||||
id: "accept-invite",
|
||||
lastSubmission,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
open &&
|
||||
navigation.formAction === "/resources/feedback" &&
|
||||
form.error === undefined &&
|
||||
form.errors.length === 0
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild={true}>{button}</SheetTrigger>
|
||||
<SheetContent size="sm">
|
||||
<SheetHeader className="justify-between">Help & feedback</SheetHeader>
|
||||
<SheetBody>
|
||||
<DiscordBanner />
|
||||
<Paragraph variant="small" className="mb-4 border-t border-slate-800 pt-3">
|
||||
Or 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 className="max-w-full">
|
||||
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
|
||||
<InputGroup className="max-w-full">
|
||||
<Label>How can we help?</Label>
|
||||
<SelectGroup>
|
||||
<Select {...conform.input(feedbackType)} defaultValue={defaultValue}>
|
||||
<SelectTrigger size="medium" width="full">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(feedbackTypeLabel).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup className="max-w-full">
|
||||
<Label>Message</Label>
|
||||
<TextArea {...conform.textarea(message)} />
|
||||
<FormError id={message.errorId}>{message.error}</FormError>
|
||||
</InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
<FormButtons
|
||||
className="w-full"
|
||||
confirmButton={
|
||||
<Button type="submit" variant="primary/medium">
|
||||
Send
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function DiscordBanner() {
|
||||
return (
|
||||
<a
|
||||
href="https://discord.gg/nkqV9xBYWy"
|
||||
target="_blank"
|
||||
className="group mb-4 flex w-full items-center justify-between rounded-md border border-slate-600 bg-gradient-to-br from-blue-400/30 to-indigo-400/50 p-4 transition hover:border-indigo-400"
|
||||
>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DiscordIcon className="h-8 w-8" />
|
||||
<h2 className="font-title text-2xl text-bright transition group-hover:text-white">
|
||||
Join the Trigger.dev
|
||||
<br />
|
||||
Discord community
|
||||
</h2>
|
||||
<Paragraph variant="small">
|
||||
Get help or answer questions from the Trigger.dev community.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<ChevronRightIcon className="h-5 w-5 text-slate-400 transition group-hover:translate-x-1 group-hover:text-indigo-400" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { EnvironmentLabel } from "./environments/EnvironmentLabel";
|
||||
import { DateTime } from "./primitives/DateTime";
|
||||
import { ActiveBadge } from "./ActiveBadge";
|
||||
|
||||
export type JobEnvironment = {
|
||||
type: RuntimeEnvironmentType;
|
||||
lastRun?: Date;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
type JobStatusTableProps = {
|
||||
environments: JobEnvironment[];
|
||||
};
|
||||
|
||||
export function JobStatusTable({ environments }: JobStatusTableProps) {
|
||||
return (
|
||||
<Table fullWidth>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Env</TableHeaderCell>
|
||||
<TableHeaderCell>Last Run</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Version</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Status</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{environments.map((environment, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<EnvironmentLabel environment={environment} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{environment.lastRun ? <DateTime date={environment.lastRun} /> : "Never Run"}
|
||||
</TableCell>
|
||||
<TableCell alignment="right">{environment.version}</TableCell>
|
||||
<TableCell alignment="right">
|
||||
<ActiveBadge active={environment.enabled} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -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"}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { Clipboard, ClipboardCheck } from "lucide-react";
|
||||
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 { Clipboard, ClipboardCheck, ClipboardCheckIcon } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
|
||||
//This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx
|
||||
//it didn't support highlighting lines by dimming the rest of the code, or animations on the highlighting
|
||||
@@ -193,21 +184,19 @@ 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]) =>
|
||||
Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
);
|
||||
|
||||
// if there are more than 1000 lines, don't highlight
|
||||
const shouldHighlight = lineCount <= 1000;
|
||||
|
||||
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 +215,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,109 +231,113 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
<Highlight
|
||||
{...defaultProps}
|
||||
theme={theme}
|
||||
code={code}
|
||||
language={language}
|
||||
>
|
||||
{({
|
||||
className: inheritedClassName,
|
||||
style: inheritedStyle,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<div
|
||||
dir="ltr"
|
||||
className="overflow-auto px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
className={cn(
|
||||
"relative mr-2 font-mono text-xs leading-relaxed",
|
||||
inheritedClassName
|
||||
)}
|
||||
style={inheritedStyle}
|
||||
{shouldHighlight ? (
|
||||
<Highlight {...defaultProps} theme={theme} code={code} language={language}>
|
||||
{({
|
||||
className: inheritedClassName,
|
||||
style: inheritedStyle,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<div
|
||||
dir="ltr"
|
||||
className="overflow-auto px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
{tokens
|
||||
.map((line, index) => {
|
||||
if (
|
||||
index === tokens.length - 1 &&
|
||||
line.length === 1 &&
|
||||
line[0].content === "\n"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
<pre
|
||||
className={cn(
|
||||
"relative mr-2 font-mono text-xs leading-relaxed",
|
||||
inheritedClassName
|
||||
)}
|
||||
style={inheritedStyle}
|
||||
dir="ltr"
|
||||
>
|
||||
{tokens
|
||||
.map((line, index) => {
|
||||
if (
|
||||
index === tokens.length - 1 &&
|
||||
line.length === 1 &&
|
||||
line[0].content === "\n"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineNumber = index + 1;
|
||||
const lineProps = getLineProps({ line, key: index });
|
||||
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)
|
||||
) {
|
||||
shouldDim = false;
|
||||
}
|
||||
let shouldDim = hasAnyHighlights;
|
||||
if (hasAnyHighlights && highlightLines?.includes(lineNumber)) {
|
||||
shouldDim = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lineProps.key}
|
||||
{...lineProps}
|
||||
className={cn(
|
||||
"flex w-full justify-start transition-opacity duration-500",
|
||||
lineProps.className
|
||||
)}
|
||||
style={{
|
||||
opacity: shouldDim ? dimAmount : undefined,
|
||||
...lineProps.style,
|
||||
}}
|
||||
>
|
||||
{showLineNumbers && (
|
||||
<div
|
||||
className={
|
||||
"mr-2 flex-none select-none text-right text-slate-500 transition-opacity duration-500"
|
||||
}
|
||||
style={{
|
||||
width: `calc(8 * ${maxLineWidth / 16}rem)`,
|
||||
}}
|
||||
>
|
||||
{lineNumber}
|
||||
return (
|
||||
<div
|
||||
key={lineProps.key}
|
||||
{...lineProps}
|
||||
className={cn(
|
||||
"flex w-full justify-start transition-opacity duration-500",
|
||||
lineProps.className
|
||||
)}
|
||||
style={{
|
||||
opacity: shouldDim ? dimAmount : undefined,
|
||||
...lineProps.style,
|
||||
}}
|
||||
>
|
||||
{showLineNumbers && (
|
||||
<div
|
||||
className={
|
||||
"mr-2 flex-none select-none text-right text-slate-500 transition-opacity duration-500"
|
||||
}
|
||||
style={{
|
||||
width: `calc(8 * ${maxLineWidth / 16}rem)`,
|
||||
}}
|
||||
>
|
||||
{lineNumber}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
{line.map((token, key) => {
|
||||
const tokenProps = getTokenProps({ token, key });
|
||||
return (
|
||||
<span
|
||||
key={tokenProps.key}
|
||||
{...tokenProps}
|
||||
style={{
|
||||
color: tokenProps?.style?.color as string,
|
||||
...tokenProps.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
{line.map((token, key) => {
|
||||
const tokenProps = getTokenProps({ token, key });
|
||||
return (
|
||||
<span
|
||||
key={tokenProps.key}
|
||||
{...tokenProps}
|
||||
style={{
|
||||
color: tokenProps?.style?.color as string,
|
||||
...tokenProps.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="w-4 flex-none" />
|
||||
</div>
|
||||
<div className="w-4 flex-none" />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Highlight>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Highlight>
|
||||
) : (
|
||||
<div
|
||||
dir="ltr"
|
||||
className="overflow-auto px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<pre className="relative mr-2 p-2 font-mono text-xs leading-relaxed" dir="ltr">
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -11,7 +8,7 @@ import { darkTheme } from "./codeMirrorTheme";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
||||
content: string;
|
||||
defaultValue?: string;
|
||||
language?: "json";
|
||||
readOnly?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
@@ -33,13 +30,14 @@ const defaultProps: JSONEditorDefaultProps = {
|
||||
|
||||
export function JSONEditor(opts: JSONEditorProps) {
|
||||
const {
|
||||
content,
|
||||
defaultValue = "",
|
||||
language,
|
||||
readOnly,
|
||||
onChange,
|
||||
onUpdate,
|
||||
onBlur,
|
||||
basicSetup,
|
||||
autoFocus,
|
||||
} = {
|
||||
...defaultProps,
|
||||
...opts,
|
||||
@@ -59,15 +57,15 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
extensions,
|
||||
editable: !readOnly,
|
||||
contentEditable: !readOnly,
|
||||
value: content,
|
||||
autoFocus: false,
|
||||
value: defaultValue,
|
||||
autoFocus,
|
||||
theme: darkTheme(),
|
||||
indentWithTab: false,
|
||||
basicSetup,
|
||||
onChange,
|
||||
onUpdate,
|
||||
};
|
||||
const { setContainer } = useCodeMirror(settings);
|
||||
const { setContainer, state } = useCodeMirror(settings);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
@@ -75,9 +73,21 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
}
|
||||
}, [setContainer]);
|
||||
|
||||
//if the defaultValue changes update the editor
|
||||
useEffect(() => {
|
||||
if (state !== undefined) {
|
||||
state.update({
|
||||
changes: { from: 0, to: state.doc.length, insert: defaultValue },
|
||||
});
|
||||
}
|
||||
}, [defaultValue, state]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("no-scrollbar overflow-y-auto", opts.className)}
|
||||
className={cn(
|
||||
"overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
|
||||
opts.className
|
||||
)}
|
||||
ref={editor}
|
||||
onBlur={() => {
|
||||
if (!onBlur) return;
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
type Environment = Pick<RuntimeEnvironment, "type" | "slug">;
|
||||
type Environment = Pick<RuntimeEnvironment, "type">;
|
||||
|
||||
export function EnvironmentLabel({
|
||||
environment,
|
||||
@@ -13,7 +13,7 @@ export function EnvironmentLabel({
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm px-1 py-0.5 text-xxs font-medium uppercase text-midnight-900",
|
||||
"inline-flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
|
||||
environmentColorClassName(environment),
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid";
|
||||
import { Link, useRevalidator } from "@remix-run/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import invariant from "tiny-invariant";
|
||||
import gradientBackground from "~/assets/images/gradient-background.png";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
|
||||
import { jobTestPath, projectStreamingPath } from "~/utils/pathBuilder";
|
||||
import { Feedback } from "../Feedback";
|
||||
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 { Header1, Header2 } from "../primitives/Headers";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
import integrationButton from "./integration-button.png";
|
||||
import selectEnvironment from "./select-environment.png";
|
||||
import selectExample from "./select-example.png";
|
||||
|
||||
type SelectionChoices = "use-existing-project" | "create-new-next-app";
|
||||
|
||||
export function HowToSetupYourProject() {
|
||||
const project = useProject();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState<SelectionChoices | null>(null);
|
||||
|
||||
invariant(devEnvironment, "devEnvironment is required");
|
||||
|
||||
const revalidator = useRevalidator();
|
||||
const events = useEventSource(projectStreamingPath(project.id), {
|
||||
event: "message",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (events !== null) {
|
||||
// This uses https://www.npmjs.com/package/canvas-confetti
|
||||
if ("confetti" in window && typeof window.confetti !== "undefined") {
|
||||
const duration = 3.5 * 1000;
|
||||
const animationEnd = Date.now() + duration;
|
||||
const defaults = {
|
||||
startVelocity: 30,
|
||||
spread: 360,
|
||||
ticks: 60,
|
||||
zIndex: 0,
|
||||
colors: [
|
||||
"#E7FF52",
|
||||
"#41FF54",
|
||||
"rgb(245 158 11)",
|
||||
"rgb(22 163 74)",
|
||||
"rgb(37 99 235)",
|
||||
"rgb(67 56 202)",
|
||||
"rgb(219 39 119)",
|
||||
"rgb(225 29 72)",
|
||||
"rgb(217 70 239)",
|
||||
],
|
||||
};
|
||||
function randomInRange(min: number, max: number): number {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
// @ts-ignore
|
||||
const interval = setInterval(function () {
|
||||
const timeLeft = animationEnd - Date.now();
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
return clearInterval(interval);
|
||||
}
|
||||
|
||||
const particleCount = 50 * (timeLeft / duration);
|
||||
// since particles fall down, start a bit higher than random
|
||||
// @ts-ignore
|
||||
window.confetti(
|
||||
Object.assign({}, defaults, {
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.1, 0.4), y: Math.random() - 0.2 },
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
window.confetti(
|
||||
Object.assign({}, defaults, {
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.6, 0.9), y: Math.random() - 0.2 },
|
||||
})
|
||||
);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
revalidator.revalidate();
|
||||
}
|
||||
// WARNING Don't put the revalidator in the useEffect deps array or bad things will happen
|
||||
}, [events]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div
|
||||
className="-ml-4 -mt-4 h-full w-[calc(100%+32px)] bg-cover bg-no-repeat pt-20"
|
||||
style={{ backgroundImage: `url("${gradientBackground}")` }}
|
||||
>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
|
||||
</Header1>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="secondary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
<RadioGroup
|
||||
className="mb-4 flex gap-x-2"
|
||||
onValueChange={(value) => setSelectedValue(value as SelectionChoices)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
label="Use an existing Next.js project"
|
||||
description="Use Trigger.dev in an existing Next.js project in less than 2 mins."
|
||||
value="use-existing-project"
|
||||
checked={selectedValue === "use-existing-project"}
|
||||
variant="icon"
|
||||
data-action="use-existing-project"
|
||||
icon={<NamedIcon className="h-12 w-12 text-green-600" name={"tree"} />}
|
||||
/>
|
||||
<RadioGroupItem
|
||||
label="Create a new Next.js project"
|
||||
description="This is the quickest way to try out Trigger.dev in a new Next.js project and takes 5 mins."
|
||||
value="create-new-next-app"
|
||||
checked={selectedValue === "create-new-next-app"}
|
||||
variant="icon"
|
||||
data-action="create-new-next-app"
|
||||
icon={<NamedIcon className="h-8 w-8 text-green-600" name={"sapling"} />}
|
||||
/>
|
||||
</RadioGroup>
|
||||
{selectedValue && (
|
||||
<>
|
||||
{selectedValue === "create-new-next-app" ? (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Create a new Next.js project" />
|
||||
<StepContentContainer>
|
||||
<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 create-next-app@latest`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm create next-app`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn create next-app`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
Trigger.dev works with either the Pages or App Router configuration.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Navigate to your new Next.js project" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
You have now created a new Next.js project. Let’s <InlineCode>cd</InlineCode>{" "}
|
||||
into it using the project name you just provided:
|
||||
</Paragraph>
|
||||
<ClipboardField
|
||||
value={"cd [replace with your project name]"}
|
||||
variant={"primary/medium"}
|
||||
></ClipboardField>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="3"
|
||||
title="Run the CLI 'init' command in your new Next.js project"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very
|
||||
simple example Job in <InlineCode variant="extra-small">examples.ts</InlineCode>{" "}
|
||||
to help you get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="4" title="Run your Next.js app" />
|
||||
<StepContentContainer>
|
||||
<NextDevCommand />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="5" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Run the CLI 'init' command in an existing Next.js project"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very
|
||||
simple example Job in <InlineCode variant="extra-small">examples.ts</InlineCode>{" "}
|
||||
to help you get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Next.js app" />
|
||||
<StepContentContainer>
|
||||
<NextDevCommand />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="4" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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="Select an environment
|
||||
"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToConnectAnIntegration() {
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Select an API from the list" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
APIs marked with a
|
||||
<span className="mx-2 -mt-1 inline-flex" aria-label="Trigger.dev Integration icon">
|
||||
<IntegrationIcon />
|
||||
</span>
|
||||
are Trigger.dev Integrations. These Integrations make connecting to the API easier by
|
||||
offering OAuth or API key authentication. All APIs can also be used with fetch or an SDK.
|
||||
</Paragraph>
|
||||
<img src={integrationButton} className="mt-2 h-10" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Choose how you want to connect" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Follow the instructions for your chosen connection method in the popover form. If no
|
||||
Integration exists yet, you can request one by clicking the "I want an Integration"
|
||||
button.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Your connection will appear in the list" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Once you've connected your API, it will appear in the list of Integrations below. You can
|
||||
view details and manage your connection by selecting it from the table.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<Callout variant={"docs"} 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({ integration, help, integrationClient }: HelpPanelProps) {
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Install the package" />
|
||||
<StepContentContainer>
|
||||
<HelpInstall packageName={integration.packageName} />
|
||||
</StepContentContainer>
|
||||
{help && (
|
||||
<>
|
||||
<StepNumber stepNumber="2" title="Create a Job" />
|
||||
<StepContentContainer>
|
||||
<HelpSamples
|
||||
help={help}
|
||||
integration={integration}
|
||||
integrationClient={integrationClient}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToDisableAJob({
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Paragraph spacing>
|
||||
To disable a job, you need to set the <InlineCode>enabled</InlineCode> property to{" "}
|
||||
<InlineCode>false</InlineCode>.
|
||||
</Paragraph>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title={
|
||||
<>
|
||||
Set <InlineCode>enabled</InlineCode> to <InlineCode>false</InlineCode>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<CodeBlock
|
||||
showLineNumbers={false}
|
||||
className="mb-4"
|
||||
code={`client.defineJob({
|
||||
id: "${id}",
|
||||
name: "${name}",
|
||||
version: "${version}",
|
||||
enabled: false,
|
||||
// ...rest of your Job definition
|
||||
});`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="2"
|
||||
title={
|
||||
<>
|
||||
Run the <InlineCode>@trigger.dev/cli dev</InlineCode> command
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
If you aren't already running the <InlineCode>dev</InlineCode> command, run it now.
|
||||
</Paragraph>
|
||||
<TriggerDevCommand />
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StepContentContainer({ children }: { children: React.ReactNode }) {
|
||||
return <div className="mb-6 ml-9 mt-1">{children}</div>;
|
||||
}
|
||||
|
||||
function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey: 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="primary/medium"
|
||||
className="mb-4"
|
||||
secure={`npx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`npx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure={`pnpm dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`pnpm dlx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure={`yarn dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`yarn dlx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
function NextDevCommand() {
|
||||
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="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>
|
||||
);
|
||||
}
|
||||
|
||||
function TriggerDevCommand() {
|
||||
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="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>
|
||||
);
|
||||
}
|
||||
|
||||
function TriggerDevStep() {
|
||||
return (
|
||||
<>
|
||||
<Paragraph spacing>
|
||||
In a <span className="text-amber-400">separate terminal window or tab</span> run:
|
||||
</Paragraph>
|
||||
<TriggerDevCommand />
|
||||
<Paragraph spacing variant="small">
|
||||
If you’re not running on port 3000 you can specify the port by adding{" "}
|
||||
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
|
||||
</Paragraph>
|
||||
<Paragraph spacing variant="small">
|
||||
You should leave the <InlineCode variant="extra-small">dev</InlineCode> command running when
|
||||
you're developing.
|
||||
</Paragraph>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,34 @@
|
||||
import { Help, Integration } from "~/services/externalApis/types";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { HelpInstall } from "./HelpInstall";
|
||||
import { HelpSamples, ReplacementData } from "./HelpSamples";
|
||||
|
||||
export type HelpPanelIntegration = Pick<Integration, "name" | "packageName" | "identifier">;
|
||||
|
||||
export type HelpPanelProps = {
|
||||
integration: HelpPanelIntegration;
|
||||
help?: Help;
|
||||
integrationClient?: ReplacementData;
|
||||
};
|
||||
|
||||
export function ApiKeyHelp({ integration, help, integrationClient }: HelpPanelProps) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Header1 className="mb-2">How to use {integration.name} with API keys</Header1>
|
||||
<Paragraph spacing>
|
||||
You can use API keys to authenticate with {integration.name}. Your API keys won't leave your
|
||||
server, we'll never see them.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
First install the <InlineCode>{integration.packageName}</InlineCode> package using your
|
||||
preferred package manager. For example:
|
||||
</Paragraph>
|
||||
<HelpInstall packageName={integration.packageName} />
|
||||
{help && (
|
||||
<HelpSamples help={help} integration={integration} integrationClient={integrationClient} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useState } from "react";
|
||||
import { ApiAuthenticationMethodApiKey, Integration } from "~/services/externalApis/types";
|
||||
import { docsIntegrationPath } from "~/utils/pathBuilder";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
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 { ApiKeyHelp } from "./ApiKeyHelp";
|
||||
import { CustomHelp } from "./CustomHelp";
|
||||
import { SelectOAuthMethod } from "./SelectOAuthMethod";
|
||||
|
||||
type IntegrationMethod = "apikey" | "oauth2" | "custom";
|
||||
|
||||
export function ConnectToIntegrationSheet({
|
||||
integration,
|
||||
organizationId,
|
||||
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 authMethods = Object.values(integration.authenticationMethods);
|
||||
const hasApiKeyOption = authMethods.some((s) => s.type === "apikey");
|
||||
const hasOAuth2Option = authMethods.some((s) => s.type === "oauth2");
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger className={className}>{button}</SheetTrigger>
|
||||
<SheetContent size="lg" className="relative">
|
||||
<SheetHeader>
|
||||
<NamedIconInBox name={icon ?? 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>
|
||||
<Header2 className="mb-2">Choose an integration method</Header2>
|
||||
<RadioGroup
|
||||
name="method"
|
||||
className="flex gap-2"
|
||||
value={integrationMethod}
|
||||
onValueChange={(v) => setIntegrationMethod(v as IntegrationMethod)}
|
||||
>
|
||||
{hasOAuth2Option && (
|
||||
<RadioGroupItem
|
||||
id="oauth2"
|
||||
value="oauth2"
|
||||
label="OAuth"
|
||||
description="We handle OAuth for you or your users."
|
||||
variant="description"
|
||||
/>
|
||||
)}
|
||||
{hasApiKeyOption && (
|
||||
<RadioGroupItem
|
||||
id="apikey"
|
||||
value="apikey"
|
||||
label="API Key"
|
||||
description="Use API keys in your code. They never leave your server."
|
||||
variant="description"
|
||||
/>
|
||||
)}
|
||||
<RadioGroupItem
|
||||
id="custom"
|
||||
value="custom"
|
||||
label="Fetch/Existing SDK"
|
||||
description={`Alternatively, use ${integration.name} without our integration.`}
|
||||
variant="description"
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
{integrationMethod && (
|
||||
<SelectedIntegrationMethod
|
||||
integration={integration}
|
||||
organizationId={organizationId}
|
||||
method={integrationMethod}
|
||||
callbackUrl={callbackUrl}
|
||||
/>
|
||||
)}
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectedIntegrationMethod({
|
||||
integration,
|
||||
organizationId,
|
||||
method,
|
||||
callbackUrl,
|
||||
}: {
|
||||
integration: Integration;
|
||||
organizationId: string;
|
||||
method: IntegrationMethod;
|
||||
callbackUrl: string;
|
||||
}) {
|
||||
const authMethods = Object.values(integration.authenticationMethods);
|
||||
|
||||
switch (method) {
|
||||
case "apikey":
|
||||
const apiAuth = authMethods.find((a) => a.type === "apikey");
|
||||
if (!apiAuth) return null;
|
||||
return <ApiKeyHelp integration={integration} help={apiAuth.help} />;
|
||||
case "oauth2":
|
||||
return (
|
||||
<SelectOAuthMethod
|
||||
integration={integration}
|
||||
organizationId={organizationId}
|
||||
callbackUrl={callbackUrl}
|
||||
/>
|
||||
);
|
||||
case "custom":
|
||||
return <CustomHelp name={integration.name} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
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 cuid from "cuid";
|
||||
import { useState } from "react";
|
||||
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 { 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 { Hint } from "../primitives/Hint";
|
||||
import { Input } from "../primitives/Input";
|
||||
import { InputGroup } from "../primitives/InputGroup";
|
||||
import { Label } from "../primitives/Label";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
export type Status = "loading" | "idle";
|
||||
|
||||
export function ConnectToOAuthForm({
|
||||
integration,
|
||||
authMethod,
|
||||
authMethodKey,
|
||||
organizationId,
|
||||
clientType,
|
||||
callbackUrl,
|
||||
}: {
|
||||
integration: Integration;
|
||||
authMethod: ApiAuthenticationMethodOAuth2;
|
||||
authMethodKey: string;
|
||||
organizationId: string;
|
||||
clientType: ConnectionType;
|
||||
callbackUrl: string;
|
||||
}) {
|
||||
const [id] = useState<string>(cuid());
|
||||
const transition = useNavigation();
|
||||
const fetcher = useFetcher();
|
||||
const { isManagedCloud } = useFeatures();
|
||||
|
||||
const [form, { title, 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))
|
||||
);
|
||||
|
||||
const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud;
|
||||
|
||||
const [useMyOAuthApp, setUseMyOAuthApp] = useState(requiresCustomOAuthApp);
|
||||
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<Scope>({
|
||||
items: authMethod.scopes,
|
||||
filter: (scope, text) => {
|
||||
if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (scope.description && scope.description.toLowerCase().includes(text.toLowerCase()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
action={`/resources/connection/${organizationId}/oauth2`}
|
||||
{...form.props}
|
||||
className="flex h-full max-h-full flex-grow flex-col"
|
||||
>
|
||||
<Fieldset>
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<input type="hidden" name="integrationIdentifier" value={integration.identifier} />
|
||||
<input type="hidden" name="integrationAuthMethod" value={authMethodKey} />
|
||||
<input type="hidden" name="redirectTo" value={location.pathname} />
|
||||
<InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">ID</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
{...conform.input(slug)}
|
||||
placeholder={`e.g. ${integration.identifier}`}
|
||||
/>
|
||||
<Hint>
|
||||
This is used in your code to reference this connection. It must be unique for this
|
||||
project.
|
||||
</Hint>
|
||||
<FormError>{slug.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
{...conform.input(title)}
|
||||
placeholder={`e.g. Personal ${integration.name}`}
|
||||
/>
|
||||
<FormError>{title.error}</FormError>
|
||||
</InputGroup>
|
||||
<input type="hidden" name="clientType" value={clientType} />
|
||||
<div>
|
||||
<Header2>Use my OAuth App</Header2>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
To use your own OAuth app, check the option below and insert the details.
|
||||
</Paragraph>
|
||||
<Checkbox
|
||||
id="hasCustomClient"
|
||||
label="Use my OAuth App"
|
||||
variant="simple/small"
|
||||
disabled={requiresCustomOAuthApp}
|
||||
onChange={(checked) => setUseMyOAuthApp(checked)}
|
||||
{...conform.input(hasCustomClient, { type: "checkbox" })}
|
||||
defaultChecked={requiresCustomOAuthApp}
|
||||
/>
|
||||
{useMyOAuthApp && (
|
||||
<div className="ml-6 mt-2">
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
Set the callback url to <CodeBlock code={callbackUrl} showLineNumbers={false} />
|
||||
</Paragraph>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client ID</Label>
|
||||
<Input fullWidth {...conform.input(customClientId, { type: "text" })} />
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client secret</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientSecret, {
|
||||
type: "password",
|
||||
})}
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<FormError>{customClientId.error}</FormError>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{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.icon ?? integration.identifier}
|
||||
>
|
||||
Connect to {integration.name}
|
||||
</Button>
|
||||
</div>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Header1, Header2 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
export function CustomHelp({ name }: { name: string }) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Header1 className="mb-2">You can use any API with requests or an SDK</Header1>
|
||||
<Header2 className="mb-2">How to use an SDK</Header2>
|
||||
<Paragraph spacing>
|
||||
You can call SDK methods from inside the run function, but you should wrap them in a Task to
|
||||
make sure they're resumable.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>Here's an example with the official GitHub SDK</Paragraph>
|
||||
<CodeBlock
|
||||
code={`
|
||||
client.defineJob({
|
||||
id: "scheduled-job-1",
|
||||
name: "Scheduled Job 1",
|
||||
version: "0.1.1",
|
||||
trigger: cronTrigger({
|
||||
cron: "*/5 * * * *", // every 5 minutes
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//wrap an SDK call in io.runTask so it's resumable and displays in logs
|
||||
const repo = await io.runTask(
|
||||
"Get repo",
|
||||
//you can add metadata to the task to improve the display in the logs
|
||||
{ name: "Get repo", icon: "github" },
|
||||
async () => {
|
||||
//this is the regular GitHub SDK
|
||||
const response = await octokit.rest.repos.get({
|
||||
owner: "triggerdotdev",
|
||||
repo: "trigger.dev",
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
`}
|
||||
highlightedRanges={[[9, 22]]}
|
||||
className="mb-4"
|
||||
/>
|
||||
<Header2 className="mb-2">How to use fetch</Header2>
|
||||
<Paragraph spacing>
|
||||
You can use the fetch API to make requests to any API. Or a different request library like
|
||||
axios if you'd prefer. Again wrapping the request in a Task will make sure it's resumable.
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
code={`
|
||||
client.defineJob({
|
||||
id: "scheduled-job-1",
|
||||
name: "Scheduled Job 1",
|
||||
version: "0.1.1",
|
||||
trigger: cronTrigger({
|
||||
cron: "*/5 * * * *", // every 5 minutes
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//wrap anything in io.runTask so it's resumable and displays in logs
|
||||
const repo = await io.runTask(
|
||||
"Get org",
|
||||
//you can add metadata to the task to improve the display in the logs
|
||||
{ name: "Get org", icon: "github" },
|
||||
async () => {
|
||||
//you can use fetch, axios, or any other library to make requests
|
||||
const response = await fetch('https://api.github.com/orgs/nodejs');
|
||||
return response.json();
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
`}
|
||||
highlightedRanges={[[9, 19]]}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { Api } from "~/services/externalApis/apis";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
import { NamedIconInBox } from "../primitives/NamedIcon";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet";
|
||||
import { CustomHelp } from "./CustomHelp";
|
||||
import { CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
export function NoIntegrationSheet({
|
||||
api,
|
||||
requested,
|
||||
button,
|
||||
}: {
|
||||
api: Api;
|
||||
requested: boolean;
|
||||
button: React.ReactNode;
|
||||
}) {
|
||||
const fetcher = useFetcher();
|
||||
const isLoading = fetcher.state !== "idle";
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger>{button}</SheetTrigger>
|
||||
<SheetContent size="lg">
|
||||
<SheetHeader className="justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<NamedIconInBox name={api.identifier} className="h-9 w-9" />
|
||||
<Header1>{api.name}</Header1>
|
||||
</div>
|
||||
{requested ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckIcon className="h-4 w-4 text-green-500" />
|
||||
<Paragraph variant="small">
|
||||
We'll let you know when the Integration is available.
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
<fetcher.Form method="post" action={`/resources/apivote/${api.identifier}`}>
|
||||
<Button
|
||||
variant="primary/small"
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? "spinner-white" : undefined}
|
||||
>
|
||||
{isLoading ? "Saving…" : `I want an Integration for ${api.name}`}
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
)}
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<CustomHelp name={api.name} />
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { useFetcher, useLocation, useNavigation } from "@remix-run/react";
|
||||
import cuid from "cuid";
|
||||
import React, { useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { useTextFilter } from "~/hooks/useTextFilter";
|
||||
import { createSchema } from "~/routes/resources.connection.$organizationId.oauth2";
|
||||
import { Integration, Scope } from "~/services/externalApis/types";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { Checkbox } from "../primitives/Checkbox";
|
||||
import { Fieldset } from "../primitives/Fieldset";
|
||||
import { FormError } from "../primitives/FormError";
|
||||
import { Header2, Header3 } from "../primitives/Headers";
|
||||
import { Input } from "../primitives/Input";
|
||||
import { InputGroup } from "../primitives/InputGroup";
|
||||
import { Label } from "../primitives/Label";
|
||||
import { NamedIcon, NamedIconInBox } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetTrigger,
|
||||
} from "../primitives/Sheet";
|
||||
|
||||
export type Status = "loading" | "idle";
|
||||
|
||||
export function OAuthConnectSheet({
|
||||
integration,
|
||||
authMethodKey,
|
||||
organizationId,
|
||||
button,
|
||||
className,
|
||||
}: {
|
||||
integration: Integration;
|
||||
authMethodKey: string;
|
||||
organizationId: string;
|
||||
button: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [id] = useState<string>(cuid());
|
||||
const transition = useNavigation();
|
||||
const fetcher = useFetcher();
|
||||
const [
|
||||
form,
|
||||
{ title, scopes, hasCustomClient, customClientId, customClientSecret },
|
||||
] = useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
// Create the schema without any constraint defined
|
||||
schema: createSchema(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const apiAuthmethod = integration.authenticationMethods[authMethodKey];
|
||||
const location = useLocation();
|
||||
|
||||
const options = [
|
||||
{ label: "Your team", value: "DEVELOPER" },
|
||||
{ label: "Your users", value: "EXTERNAL" },
|
||||
];
|
||||
|
||||
const [selectedScopes, setSelectedScopes] = useState<Set<string>>(
|
||||
new Set(
|
||||
apiAuthmethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name)
|
||||
)
|
||||
);
|
||||
|
||||
const [useMyOAuthApp, setUseMyOAuthApp] = useState(false);
|
||||
|
||||
const [clientType, setClientType] = useState<"EXTERNAL" | "DEVELOPER">(
|
||||
"DEVELOPER"
|
||||
);
|
||||
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<Scope>({
|
||||
items: apiAuthmethod.scopes,
|
||||
filter: (scope, text) => {
|
||||
if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (
|
||||
scope.description &&
|
||||
scope.description.toLowerCase().includes(text.toLowerCase())
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger className={className}>{button}</SheetTrigger>
|
||||
<SheetContent size="lg">
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
action={`/resources/connection/${organizationId}/oauth2`}
|
||||
{...form.props}
|
||||
className="flex h-full max-h-full flex-grow flex-col"
|
||||
>
|
||||
<SheetBody>
|
||||
<div className="flex items-center gap-4 border-b border-slate-800 pb-3.5">
|
||||
<NamedIconInBox
|
||||
name={integration.identifier}
|
||||
className="h-9 w-9"
|
||||
/>
|
||||
<div>
|
||||
<Header2>Connect to {integration.name}</Header2>
|
||||
<Paragraph variant="extra-small">
|
||||
{apiAuthmethod.name}{" "}
|
||||
{apiAuthmethod.description &&
|
||||
`– ${apiAuthmethod.description}`}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Fieldset>
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="integrationIdentifier"
|
||||
value={integration.identifier}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="integrationAuthMethod"
|
||||
value={authMethodKey}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="redirectTo"
|
||||
value={location.pathname}
|
||||
/>
|
||||
<InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
{...conform.input(title)}
|
||||
placeholder={`e.g. Personal ${integration.name}`}
|
||||
/>
|
||||
<FormError>{title.error}</FormError>
|
||||
</InputGroup>
|
||||
<Callout variant="info">
|
||||
Coming soon – create connections so you can run Jobs with your
|
||||
users credentials.
|
||||
</Callout>
|
||||
{/* <div>
|
||||
<Header2>Who will connect to this API?</Header2>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
Select ‘Your team’ if you want create internal Jobs for your
|
||||
own company. Select 'Your users' if you want to create Jobs
|
||||
where your customers authenticate with this API.
|
||||
</Paragraph>
|
||||
<FormSegmentedControl
|
||||
name="clientType"
|
||||
defaultValue={clientType}
|
||||
options={options}
|
||||
onChange={(val) => setClientType(val as any)}
|
||||
/>
|
||||
</div> */}
|
||||
<input type="hidden" name="clientType" value={"DEVELOPER"} />
|
||||
<div>
|
||||
<Header2>Use my OAuth App</Header2>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
To use your own OAuth app, check the option below and insert
|
||||
the details.
|
||||
</Paragraph>
|
||||
<Checkbox
|
||||
id="hasCustomClient"
|
||||
label="Use my OAuth App"
|
||||
variant="simple/small"
|
||||
defaultChecked={
|
||||
clientType === "EXTERNAL" ? true : useMyOAuthApp
|
||||
}
|
||||
disabled={clientType === "EXTERNAL"}
|
||||
onChange={(checked) => setUseMyOAuthApp(checked)}
|
||||
{...conform.input(hasCustomClient, { type: "checkbox" })}
|
||||
/>
|
||||
{useMyOAuthApp && (
|
||||
<div className="ml-6 mt-2">
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
Set the callback url to{" "}
|
||||
<CodeBlock
|
||||
code={`${origin}/oauth2/callback`}
|
||||
showLineNumbers={false}
|
||||
/>
|
||||
</Paragraph>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client ID</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientId, { type: "text" })}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client secret</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientSecret, {
|
||||
type: "password",
|
||||
})}
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<FormError>{customClientId.error}</FormError>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Header2>Scopes</Header2>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Select the scopes you want to grant to {integration.name} in
|
||||
order for it to access your data. Note: If you try and perform
|
||||
an action in a Job that requires a scope you 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="flex flex-col gap-y-0.5 overflow-hidden rounded-md">
|
||||
{filteredItems.length === 0 && (
|
||||
<Paragraph variant="small" className="p-4">
|
||||
No scopes match {filterText}. Try a different search
|
||||
query.
|
||||
</Paragraph>
|
||||
)}
|
||||
{apiAuthmethod.scopes.map((s) => {
|
||||
return (
|
||||
<Checkbox
|
||||
key={s.name}
|
||||
id={s.name}
|
||||
value={s.name}
|
||||
name="scopes"
|
||||
label={s.name}
|
||||
defaultChecked={s.defaultChecked ?? false}
|
||||
badges={s.annotations?.map((a) => a.label)}
|
||||
description={s.description}
|
||||
variant="description"
|
||||
className={cn(
|
||||
filteredItems.find((f) => f.name === s.name)
|
||||
? ""
|
||||
: "hidden"
|
||||
)}
|
||||
onChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.add(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
} else {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.delete(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Fieldset>
|
||||
</SheetBody>
|
||||
<SheetFooter>
|
||||
<div className="flex items-center justify-end gap-x-4">
|
||||
<FormError>{scopes.error}</FormError>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex gap-2"
|
||||
disabled={transition.state !== "idle"}
|
||||
variant="primary/medium"
|
||||
>
|
||||
<>
|
||||
<NamedIcon
|
||||
name={integration.identifier}
|
||||
className={"h-4 w-4"}
|
||||
/>
|
||||
Connect to {integration.name}
|
||||
</>
|
||||
</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</fetcher.Form>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useState } from "react";
|
||||
import { ApiAuthenticationMethodOAuth2, Integration } from "~/services/externalApis/types";
|
||||
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
|
||||
import type { ConnectionType } from "@trigger.dev/database";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { ConnectToOAuthForm } from "./ConnectToOAuthForm";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Client } from "~/presenters/IntegrationsPresenter.server";
|
||||
import { UpdateOAuthForm } from "./UpdateOAuthForm";
|
||||
|
||||
export function SelectOAuthMethod({
|
||||
integration,
|
||||
organizationId,
|
||||
callbackUrl,
|
||||
existingIntegration,
|
||||
}: {
|
||||
existingIntegration?: Client;
|
||||
integration: Integration;
|
||||
organizationId: string;
|
||||
callbackUrl: string;
|
||||
}) {
|
||||
const oAuthMethods = Object.entries(integration.authenticationMethods).filter(
|
||||
(a): a is [string, ApiAuthenticationMethodOAuth2] => a[1].type === "oauth2"
|
||||
);
|
||||
|
||||
const [oAuthKey, setOAuthKey] = useState<string | undefined>(
|
||||
oAuthMethods.length === 1 ? oAuthMethods[0][0] : undefined
|
||||
);
|
||||
const [connectionType, setConnectionType] = useState<ConnectionType | undefined>();
|
||||
|
||||
const selectedOAuthMethod = oAuthKey
|
||||
? (integration.authenticationMethods[oAuthKey] as ApiAuthenticationMethodOAuth2)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{oAuthMethods.length > 1 && (
|
||||
<>
|
||||
<Header2 className="mb-2 mt-4">Select an OAuth option</Header2>
|
||||
<RadioGroup
|
||||
name="oauth-method"
|
||||
className="flex gap-2"
|
||||
value={oAuthKey}
|
||||
onValueChange={(v: any) => setOAuthKey(v)}
|
||||
>
|
||||
{oAuthMethods.map(([key, auth]) => (
|
||||
<RadioGroupItem
|
||||
key={key}
|
||||
id={key}
|
||||
value={key}
|
||||
label={auth.name}
|
||||
description={auth.description}
|
||||
variant="description"
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</>
|
||||
)}
|
||||
{selectedOAuthMethod && (
|
||||
<>
|
||||
<Header2 className="mb-2 mt-4">Who is connecting to {integration.name}</Header2>
|
||||
<RadioGroup
|
||||
name="connection-type"
|
||||
className="flex gap-2"
|
||||
value={connectionType}
|
||||
onValueChange={(v: any) => setConnectionType(v as ConnectionType)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id="DEVELOPER"
|
||||
value="DEVELOPER"
|
||||
label="Developers"
|
||||
description={`You will connect using an internal ${integration.name} account.`}
|
||||
variant="description"
|
||||
/>
|
||||
<RadioGroupItem
|
||||
id="EXTERNAL"
|
||||
value="EXTERNAL"
|
||||
label="Your users"
|
||||
description="We will give you OAuth React components so you can connect as your users."
|
||||
variant="description"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</>
|
||||
)}
|
||||
{selectedOAuthMethod &&
|
||||
connectionType &&
|
||||
oAuthKey &&
|
||||
(connectionType === "DEVELOPER" ? (
|
||||
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">
|
||||
message us
|
||||
</a>
|
||||
.
|
||||
</Paragraph>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ConnectionType } from "@trigger.dev/database";
|
||||
|
||||
export function connectionType(type: ConnectionType) {
|
||||
switch (type) {
|
||||
case "DEVELOPER":
|
||||
return "Developer";
|
||||
case "EXTERNAL":
|
||||
return "Your users";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { JobStatusTable } from "../JobsStatusTable";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Header1, Header2 } from "../primitives/Headers";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
|
||||
type JobEnvironment = {
|
||||
type: RuntimeEnvironmentType;
|
||||
lastRun?: Date;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
type DeleteJobDialogContentProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
environments: JobEnvironment[];
|
||||
redirectTo?: string;
|
||||
};
|
||||
|
||||
export function DeleteJobDialogContent({
|
||||
title,
|
||||
slug,
|
||||
environments,
|
||||
id,
|
||||
redirectTo,
|
||||
}: DeleteJobDialogContentProps) {
|
||||
const canDelete = environments.every((environment) => !environment.enabled);
|
||||
const fetcher = useFetcher();
|
||||
|
||||
const isLoading =
|
||||
fetcher.state === "submitting" ||
|
||||
(fetcher.state === "loading" && fetcher.formMethod === "DELETE");
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-y-6">
|
||||
<div className="flex flex-col items-center justify-center gap-y-2">
|
||||
<Header1>{title}</Header1>
|
||||
<Paragraph variant="small">ID: {slug}</Paragraph>
|
||||
</div>
|
||||
<JobStatusTable environments={environments} />
|
||||
|
||||
<Header2
|
||||
className={cn(
|
||||
canDelete ? "border-rose-500 bg-rose-500/10" : "border-amber-500 bg-amber-500/10",
|
||||
"rounded border px-3.5 py-2 text-center text-bright"
|
||||
)}
|
||||
>
|
||||
{canDelete
|
||||
? "Are you sure you want to delete this Job?"
|
||||
: "You can't delete this Job until all env are disabled"}
|
||||
</Header2>
|
||||
<Paragraph variant="small" className="px-6 text-center">
|
||||
{canDelete ? (
|
||||
<>
|
||||
This will permanently delete the Job <span className="strong text-bright">{title}</span>
|
||||
. This includes the deletion of all Run history. This cannot be undone.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
This Job is still active in an environment. You need to disable it in your Job code
|
||||
first before it can be deleted.{" "}
|
||||
<TextLink to="https://trigger.dev/docs/documentation/guides/jobs/managing#disabling-jobs">
|
||||
Learn how to disable a Job
|
||||
</TextLink>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</Paragraph>
|
||||
{canDelete ? (
|
||||
<fetcher.Form
|
||||
method="delete"
|
||||
action={`/resources/jobs/${id}${redirectTo ? `?redirectTo=${redirectTo}` : ""}`}
|
||||
className="w-full"
|
||||
>
|
||||
<Button variant="danger/large" fullWidth>
|
||||
{isLoading ? (
|
||||
<Spinner color="white" />
|
||||
) : (
|
||||
<>
|
||||
<NamedIcon
|
||||
name="trash-can"
|
||||
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
|
||||
/>
|
||||
Delete this Job
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
) : (
|
||||
<Button variant="danger/large" fullWidth disabled>
|
||||
<>
|
||||
<NamedIcon
|
||||
name="trash-can"
|
||||
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
|
||||
/>
|
||||
Delete this Job
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ActiveBadge, MissingIntegrationBadge, NewBadge } from "../ActiveBadge";
|
||||
|
||||
type JobStatusBadgeProps = {
|
||||
enabled: boolean;
|
||||
hasIntegrationsRequiringAction: boolean;
|
||||
hasRuns: boolean;
|
||||
badgeSize?: "small" | "normal";
|
||||
};
|
||||
|
||||
export function JobStatusBadge({
|
||||
enabled,
|
||||
hasIntegrationsRequiringAction,
|
||||
hasRuns,
|
||||
badgeSize = "normal",
|
||||
}: JobStatusBadgeProps) {
|
||||
if (!enabled) {
|
||||
return <ActiveBadge active={false} badgeSize={badgeSize} />;
|
||||
}
|
||||
|
||||
if (hasIntegrationsRequiringAction) {
|
||||
return <MissingIntegrationBadge badgeSize={badgeSize} />;
|
||||
}
|
||||
|
||||
if (!hasRuns) {
|
||||
return <NewBadge badgeSize={badgeSize} />;
|
||||
}
|
||||
|
||||
return <ActiveBadge active={true} badgeSize={badgeSize} />;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import { ProjectJob } from "~/hooks/useJobs";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { JobRunStatus } from "~/models/job.server";
|
||||
import { jobPath, jobTestPath } from "~/utils/pathBuilder";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
|
||||
import { LabelValueStack } from "../primitives/LabelValueStack";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { PopoverMenuItem } from "../primitives/Popover";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellMenu,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "../primitives/Table";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
import { runStatusTitle } from "../runs/RunStatuses";
|
||||
import { DeleteJobDialogContent } from "./DeleteJobModalContent";
|
||||
import { JobStatusBadge } from "./JobStatusBadge";
|
||||
|
||||
export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Job</TableHeaderCell>
|
||||
<TableHeaderCell>ID</TableHeaderCell>
|
||||
<TableHeaderCell>Integrations</TableHeaderCell>
|
||||
<TableHeaderCell>Properties</TableHeaderCell>
|
||||
<TableHeaderCell>Last run</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{jobs.length > 0 ? (
|
||||
jobs.map((job) => {
|
||||
const path = jobPath(organization, project, job);
|
||||
return (
|
||||
<TableRow key={job.id} className="group">
|
||||
<TableCell to={path}>
|
||||
<span className="flex items-center gap-2">
|
||||
<NamedIcon name={job.event.icon} className="h-8 w-8" />
|
||||
<LabelValueStack
|
||||
label={job.title}
|
||||
value={
|
||||
job.dynamic ? (
|
||||
<span className="flex items-center gap-0.5">
|
||||
<NamedIcon name="dynamic" className="h-4 w-4" />{" "}
|
||||
<span className="uppercase">Dynamic:</span> {job.event.title}
|
||||
</span>
|
||||
) : (
|
||||
job.event.title
|
||||
)
|
||||
}
|
||||
variant="primary"
|
||||
/>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<LabelValueStack label={job.slug} value={`v${job.version}`} variant="primary" />
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{job.integrations.map((integration) => (
|
||||
<SimpleTooltip
|
||||
key={integration.key}
|
||||
button={
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{job.properties && (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<div className="flex max-w-[200px] items-start justify-start gap-5 truncate">
|
||||
{job.properties.map((property, index) => (
|
||||
<LabelValueStack
|
||||
key={index}
|
||||
label={property.label}
|
||||
value={property.text}
|
||||
className=" last:truncate"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
{job.properties.map((property, index) => (
|
||||
<LabelValueStack
|
||||
key={index}
|
||||
label={property.label}
|
||||
value={property.text}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{job.lastRun ? (
|
||||
<LabelValueStack
|
||||
label={
|
||||
<span className={classForJobStatus(job.lastRun.status)}>
|
||||
{runStatusTitle(job.lastRun.status)}
|
||||
</span>
|
||||
}
|
||||
value={
|
||||
<span className={classForJobStatus(job.lastRun.status)}>
|
||||
<DateTime date={job.lastRun.createdAt} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LabelValueStack label={"Never run"} value={"–"} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<JobStatusBadge
|
||||
enabled={job.status === "ACTIVE"}
|
||||
hasIntegrationsRequiringAction={job.hasIntegrationsRequiringAction}
|
||||
hasRuns={job.lastRun !== undefined}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCellMenu isSticky>
|
||||
<PopoverMenuItem to={path} title="View Job" icon="eye" />
|
||||
<PopoverMenuItem
|
||||
to={jobTestPath(organization, project, job)}
|
||||
title="Test Job"
|
||||
icon="beaker"
|
||||
/>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="menu-item" LeadingIcon="trash-can">
|
||||
Delete Job
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DeleteJobDialogContent
|
||||
id={job.id}
|
||||
title={job.title}
|
||||
slug={job.slug}
|
||||
environments={job.environments}
|
||||
/>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TableCellMenu>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={6}>
|
||||
<Paragraph variant="small" className="flex items-center justify-center">
|
||||
{noResultsText}
|
||||
</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function classForJobStatus(status: JobRunStatus) {
|
||||
switch (status) {
|
||||
case "FAILURE":
|
||||
case "TIMED_OUT":
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
case "PENDING":
|
||||
return "text-rose-500";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -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,19 +52,15 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
export function MainCenteredContainer({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export function PageBodyPadding({ children }: { children: React.ReactNode }) {
|
||||
return <div className="p-4">{children}</div>;
|
||||
}
|
||||
|
||||
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,147 +1,26 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { Fragment } from "react";
|
||||
import { useJob, useOptionalJob } from "~/hooks/useJob";
|
||||
import {
|
||||
useOptionalOrganization,
|
||||
useOrganization,
|
||||
} from "~/hooks/useOrganizations";
|
||||
import { useOptionalProject, useProject } from "~/hooks/useProject";
|
||||
import {
|
||||
jobPath,
|
||||
projectEnvironmentsPath,
|
||||
projectIntegrationsPath,
|
||||
projectPath,
|
||||
runPath,
|
||||
} 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";
|
||||
import { useOptionalRun, useRun } from "~/hooks/useRun";
|
||||
|
||||
export type Breadcrumb = {
|
||||
slug:
|
||||
| "projects"
|
||||
| "jobs"
|
||||
| "integrations"
|
||||
| "environments"
|
||||
| "job"
|
||||
| "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 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}
|
||||
/>
|
||||
</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,
|
||||
}: {
|
||||
breadcrumb: Breadcrumb;
|
||||
organization?: ReturnType<typeof useOrganization>;
|
||||
project?: ReturnType<typeof useProject>;
|
||||
job?: ReturnType<typeof useJob>;
|
||||
run?: ReturnType<typeof useRun>;
|
||||
}) {
|
||||
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 "job":
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={projectPath(organization!, project!)}
|
||||
title="Jobs"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<JobsMenu key={breadcrumb.slug} />
|
||||
</Fragment>
|
||||
);
|
||||
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={runPath(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 (
|
||||
<>
|
||||
@@ -28,30 +28,32 @@ export function JobsMenu() {
|
||||
<PopoverArrowTrigger isOpen={isOpen}>
|
||||
{currentJob?.title ?? "Select a job"}
|
||||
</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}
|
||||
>
|
||||
<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 { ChatBubbleLeftRightIcon } from "@heroicons/react/24/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 { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
|
||||
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-uiBorder 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,21 +20,20 @@ export function NavBar() {
|
||||
<MobileDropdownMenu />
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
<LinkButton
|
||||
to="https://docs.trigger.dev"
|
||||
variant="secondary/small"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
>
|
||||
<LinkButton to={docsRoot()} variant="secondary/small" LeadingIcon={BookOpenIcon}>
|
||||
Documentation
|
||||
</LinkButton>
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
data-attr="posthog-feedback-button"
|
||||
LeadingIcon={ChatBubbleLeftRightIcon}
|
||||
shortcut="F"
|
||||
>
|
||||
Send us feedback
|
||||
</Button>
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
LeadingIcon={ChatBubbleLeftRightIcon}
|
||||
shortcut={{ key: "f" }}
|
||||
>
|
||||
Help & feedback
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -83,7 +82,7 @@ function MobileDropdownMenu() {
|
||||
className="absolute inset-x-6 top-0 mt-20 flex origin-top flex-col gap-4 rounded-md bg-slate-800 p-4 text-lg tracking-tight text-slate-900 shadow-xl ring-1 ring-slate-700"
|
||||
>
|
||||
{/* <PrimaryA
|
||||
href="https://docs.trigger.dev"
|
||||
href={docsRoot()}
|
||||
target="_blank"
|
||||
className="max-w-full"
|
||||
>
|
||||
@@ -122,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,28 +12,28 @@ 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 { 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";
|
||||
const collapsedWith = "2.81rem";
|
||||
|
||||
const menuVariants = {
|
||||
expanded: {
|
||||
minWidth: "11rem",
|
||||
width: "11rem",
|
||||
minWidth: expandedWith,
|
||||
width: expandedWith,
|
||||
},
|
||||
collapsed: {
|
||||
minWidth: "2.81rem",
|
||||
width: "2.81rem",
|
||||
minWidth: collapsedWith,
|
||||
width: collapsedWith,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,14 +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 isCollapsed = job !== 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
|
||||
@@ -57,7 +58,7 @@ export function ProjectSideMenu() {
|
||||
variants={menuVariants}
|
||||
initial={isCollapsed ? "collapsed" : "expanded"}
|
||||
className={cn(
|
||||
"flex h-full flex-col justify-between overflow-hidden border-r border-slate-850 p-1 transition duration-300 ease-in-out"
|
||||
"flex h-full flex-col justify-between overflow-hidden border-r border-uiBorder p-1 transition duration-300 ease-in-out"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -67,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="Environments"
|
||||
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">
|
||||
@@ -87,18 +100,21 @@ export function ProjectSideMenu() {
|
||||
icon="team"
|
||||
to={organizationTeamPath(organization)}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="team"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Billing"
|
||||
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>
|
||||
@@ -120,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 (
|
||||
@@ -142,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"
|
||||
@@ -149,13 +168,14 @@ function SideMenuItem({
|
||||
}}
|
||||
>
|
||||
<motion.span
|
||||
className="pl-1"
|
||||
className="shrink-0 pl-1"
|
||||
animate={isCollapsed ? "collapsed" : "expanded"}
|
||||
variants={itemVariants}
|
||||
initial={isCollapsed ? "collapsed" : "expanded"}
|
||||
>
|
||||
{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,18 @@ 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) {
|
||||
return <div className={cn(variants[variant], className)} {...props} />;
|
||||
export function Badge({ className, variant = "default", children, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(variants[variant], className)} {...props}>
|
||||
<span className="-mb-0.5">{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,89 +1,82 @@
|
||||
import { LinkProps, NavLink, NavLinkProps } from "@remix-run/react";
|
||||
import { Link } from "@remix-run/react";
|
||||
import React from "react";
|
||||
import { Link, LinkProps, NavLink, NavLinkProps } from "@remix-run/react";
|
||||
import React, { ReactComponentElement, forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import type { IconNames } from "./NamedIcon";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { 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",
|
||||
shortcutVariant: "small" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-bright/40 text-bright group-hover:border-bright/60 transition",
|
||||
"ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60 justify-self-center",
|
||||
},
|
||||
"secondary/small": {
|
||||
textColor:
|
||||
"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",
|
||||
shortcutVariant: "small" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition",
|
||||
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
|
||||
},
|
||||
"tertiary/small": {
|
||||
textColor:
|
||||
"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",
|
||||
shortcutVariant: "small" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition",
|
||||
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
|
||||
},
|
||||
"danger/small": {
|
||||
textColor:
|
||||
"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",
|
||||
shortcut:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-bright/40 text-bright group-hover:border-bright/60 transition",
|
||||
shortcutVariant: "small" as const,
|
||||
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",
|
||||
shortcut:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-bright group-hover:border-bright/60 transition",
|
||||
shortcutVariant: "medium" as const,
|
||||
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:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright transition",
|
||||
"ml-1.5 -mr-0.5 border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright",
|
||||
},
|
||||
"tertiary/medium": {
|
||||
textColor:
|
||||
"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:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright transition",
|
||||
"ml-1.5 -mr-0.5 border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright",
|
||||
},
|
||||
"danger/medium": {
|
||||
textColor:
|
||||
"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",
|
||||
shortcut:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-bright group-hover:border-bright/60 transition",
|
||||
shortcutVariant: "medium" as const,
|
||||
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",
|
||||
shortcutVariant: undefined,
|
||||
shortcut: undefined,
|
||||
},
|
||||
"secondary/large": {
|
||||
@@ -91,13 +84,23 @@ const variant = {
|
||||
button:
|
||||
"h-10 px-2 text-sm text-dimmed group-hover:text-bright transition font-medium bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
|
||||
icon: "h-5",
|
||||
shortcutVariant: undefined,
|
||||
shortcut: undefined,
|
||||
},
|
||||
"danger/large": {
|
||||
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50",
|
||||
button:
|
||||
"h-10 px-2 text-md bg-rose-600 group-hover:bg-rose-500 group-disabled:opacity-50 group-disabled:group-hover:bg-rose-600",
|
||||
icon: "h-5",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
},
|
||||
"menu-item": {
|
||||
textColor: "text-bright",
|
||||
button:
|
||||
"h-9 px-[0.475rem] text-sm rounded-sm bg-transparent group-hover:bg-slate-800 transition",
|
||||
icon: "h-5",
|
||||
shortcutVariant: undefined,
|
||||
shortcut: undefined,
|
||||
},
|
||||
};
|
||||
@@ -109,14 +112,14 @@ 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;
|
||||
textAlignLeft?: boolean;
|
||||
className?: string;
|
||||
shortcut?: string;
|
||||
shortcut?: ShortcutDefinition;
|
||||
variant: keyof typeof variant;
|
||||
};
|
||||
|
||||
@@ -132,23 +135,16 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
textAlignLeft,
|
||||
className,
|
||||
} = props;
|
||||
const variation = allVariants.variant[props.variant];
|
||||
|
||||
// Based on the size prop, we'll use the corresponding variant classnames
|
||||
const btnClassName = cn(
|
||||
allVariants.$all,
|
||||
allVariants.variant[props.variant].button
|
||||
);
|
||||
const iconClassName = allVariants.variant[props.variant].icon;
|
||||
const shortcutClassName = allVariants.variant[props.variant].shortcut;
|
||||
const textColorClassName = allVariants.variant[props.variant].textColor;
|
||||
const btnClassName = cn(allVariants.$all, variation.button);
|
||||
const iconClassName = variation.icon;
|
||||
const shortcutClassName = variation.shortcut;
|
||||
const textColorClassName = variation.textColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
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",
|
||||
@@ -159,11 +155,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
|
||||
@@ -178,13 +170,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>
|
||||
) : (
|
||||
@@ -195,11 +181,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
|
||||
@@ -211,7 +193,13 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{shortcut && <span className={cn(shortcutClassName)}>{shortcut}</span>}
|
||||
{shortcut && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -223,8 +211,23 @@ type ButtonPropsType = Pick<
|
||||
> &
|
||||
React.ComponentProps<typeof ButtonContent>;
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
({ type, disabled, onClick, ...props }, ref) => {
|
||||
const innerRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement);
|
||||
|
||||
if (props.shortcut) {
|
||||
useShortcutKeys({
|
||||
shortcut: props.shortcut,
|
||||
action: () => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.click();
|
||||
}
|
||||
},
|
||||
disabled,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
@@ -233,7 +236,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
onClick={onClick}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
ref={ref}
|
||||
ref={innerRef}
|
||||
form={props.form}
|
||||
>
|
||||
<ButtonContent {...props} />
|
||||
@@ -242,13 +245,25 @@ export const Button = React.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) {
|
||||
useShortcutKeys({
|
||||
shortcut: props.shortcut,
|
||||
action: () => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.click();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (to.toString().startsWith("http")) {
|
||||
return (
|
||||
<ExtLink
|
||||
href={to.toString()}
|
||||
ref={innerRef}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
>
|
||||
<ButtonContent {...props} />
|
||||
@@ -258,6 +273,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
ref={innerRef}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
>
|
||||
<ButtonContent {...props} />
|
||||
@@ -268,26 +284,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>
|
||||
);
|
||||
@@ -299,16 +302,19 @@ type ExtLinkProps = JSX.IntrinsicElements["a"] & {
|
||||
href: string;
|
||||
};
|
||||
|
||||
function ExtLink({ className, href, children, ...props }: ExtLinkProps) {
|
||||
return (
|
||||
<a
|
||||
className={cn(className)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const ExtLink = forwardRef<HTMLAnchorElement, ExtLinkProps>(
|
||||
({ className, href, children, ...props }, ref) => {
|
||||
return (
|
||||
<a
|
||||
className={cn(className)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,29 +4,52 @@ import {
|
||||
InformationCircleIcon,
|
||||
LightBulbIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
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 text-blue-400" />,
|
||||
text: "text-blue-200",
|
||||
icon: <InformationCircleIcon className="h-5 w-5 shrink-0 text-blue-400" />,
|
||||
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 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 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 text-green-400" />,
|
||||
text: "text-green-200",
|
||||
icon: <LightBulbIcon className="h-5 w-5 shrink-0 text-green-400" />,
|
||||
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" />,
|
||||
textColor: "text-green-200",
|
||||
linkClassName: "transition hover:bg-green-400/40",
|
||||
},
|
||||
docs: {
|
||||
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;
|
||||
|
||||
@@ -35,33 +58,88 @@ export function Callout({
|
||||
className,
|
||||
icon,
|
||||
variant,
|
||||
to,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
variant: keyof typeof variantClasses;
|
||||
to?: string;
|
||||
}) {
|
||||
const variantDefinition = variantClasses[variant];
|
||||
|
||||
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 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 (
|
||||
<div
|
||||
className={cn(
|
||||
`flex w-full gap-4 rounded-md border py-3 pl-3 pr-4 shadow-md backdrop-blur-sm`,
|
||||
`flex w-full items-start gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-start gap-2.5">
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.text}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.textColor}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const variants = {
|
||||
},
|
||||
description: {
|
||||
button: "w-full py-2 pl-3 pr-4 checked:hover:bg-slate-850 transition",
|
||||
label: "text-bright font-mono",
|
||||
label: "text-bright font-semibold",
|
||||
description: "text-dimmed",
|
||||
inputPosition: "mt-1",
|
||||
isChecked: "bg-slate-850",
|
||||
@@ -59,7 +59,6 @@ export type CheckboxProps = Omit<
|
||||
variant?: keyof typeof variants;
|
||||
label?: string;
|
||||
description?: string;
|
||||
badge?: string;
|
||||
badges?: string[];
|
||||
className?: string;
|
||||
onChange?: (isChecked: boolean) => void;
|
||||
@@ -76,7 +75,6 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
label,
|
||||
description,
|
||||
defaultChecked,
|
||||
badge,
|
||||
badges,
|
||||
disabled,
|
||||
className,
|
||||
@@ -84,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;
|
||||
@@ -159,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 h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
className={cn("inline-flex items-center justify-center transition duration-100", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -28,7 +25,7 @@ const ClientTabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex items-center justify-center whitespace-nowrap border-r border-slate-700 px-2 text-sm ring-offset-background transition-all first:pl-0 last:border-none data-[state=active]:text-indigo-500 data-[state=inactive]:text-dimmed data-[state=inactive]:hover:text-bright focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -43,7 +40,7 @@ const ClientTabsContent = React.forwardRef<
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
"mt-1 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Button } from "./Buttons";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { CheckIcon } from "@heroicons/react/20/solid";
|
||||
import { IconNames, NamedIcon } from "./NamedIcon";
|
||||
|
||||
const variations = {
|
||||
"primary/small": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-800 bg-slate-850 text-xs transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-slate-850 border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "primary/small" as const,
|
||||
button: "rounded-l-none min-w-[3.1rem]",
|
||||
iconSize: "h-3 w-3",
|
||||
iconPadding: "pl-1",
|
||||
},
|
||||
"secondary/small": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-750 bg-slate-850 text-xs transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-slate-850 border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "secondary/small" as const,
|
||||
button: "rounded-l-none border-l border-slate-750 min-w-[3.1rem]",
|
||||
iconSize: "h-3 w-3",
|
||||
iconPadding: "pl-1",
|
||||
},
|
||||
"tertiary/small": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-850 bg-transparent text-xs transition hover:border-slate-800 hover:bg-slate-950 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "tertiary/small" as const,
|
||||
button: "rounded-l-none border-l border-slate-850 min-w-[3.1rem]",
|
||||
iconSize: "h-3 w-3",
|
||||
iconPadding: "pl-1",
|
||||
},
|
||||
"primary/medium": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-800 bg-slate-850 text-sm transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-slate-850 border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "primary/medium" as const,
|
||||
button: "rounded-l-none min-w-[4rem]",
|
||||
iconSize: "h-4 w-4",
|
||||
iconPadding: "pl-2",
|
||||
},
|
||||
"secondary/medium": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-750 bg-slate-850 text-sm transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-slate-850 border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "secondary/medium" as const,
|
||||
button: "rounded-l-none border-l border-slate-750 min-w-[4rem]",
|
||||
iconSize: "h-4 w-4",
|
||||
iconPadding: "pl-2",
|
||||
},
|
||||
"tertiary/medium": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-850 bg-transparent text-sm transition hover:border-slate-800 hover:bg-slate-950 focus:border-4 focus:border-solid focus:border-l-transparent",
|
||||
input:
|
||||
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "tertiary/medium" as const,
|
||||
button: "rounded-l-none border-l border-slate-850 min-w-[4rem]",
|
||||
iconSize: "h-4 w-4",
|
||||
iconPadding: "pl-2",
|
||||
},
|
||||
};
|
||||
|
||||
type ClipboardFieldProps = {
|
||||
value: string;
|
||||
secure?: boolean | string;
|
||||
variant: keyof typeof variations;
|
||||
className?: string;
|
||||
icon?: IconNames | React.ReactNode;
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
export function ClipboardField({
|
||||
value,
|
||||
secure = false,
|
||||
variant,
|
||||
className,
|
||||
icon,
|
||||
fullWidth = true,
|
||||
}: ClipboardFieldProps) {
|
||||
const [isSecure, setIsSecure] = useState(secure !== undefined && secure);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1500);
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const { container, input, buttonVariant, button } = variations[variant];
|
||||
const iconClassName = variations[variant].iconSize;
|
||||
const iconPosition = variations[variant].iconPadding;
|
||||
const inputIcon = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<span className={cn(container, fullWidth ? "w-full" : "max-w-max", className)}>
|
||||
{icon && (
|
||||
<span
|
||||
onClick={() => inputIcon.current && inputIcon.current.focus()}
|
||||
className={cn(iconPosition, "flex items-center")}
|
||||
>
|
||||
{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}
|
||||
readOnly={true}
|
||||
className={cn("shrink grow select-all overflow-x-auto", input)}
|
||||
size={value.length}
|
||||
onFocus={(e) => {
|
||||
if (secure) {
|
||||
setIsSecure((i) => false);
|
||||
}
|
||||
e.currentTarget.select();
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (secure) {
|
||||
setIsSecure((i) => true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button variant={buttonVariant} onClick={copy} className={cn("shrink-0 grow-0", button)}>
|
||||
{copied ? <CheckIcon className="h-4 w-4 text-green-500" /> : "Copy"}
|
||||
</Button>
|
||||
</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}`;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
|
||||
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}
|
||||
@@ -29,7 +26,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100",
|
||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -46,44 +43,43 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in sm:max-w-lg sm:rounded-lg sm:zoom-in-90",
|
||||
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-midnight-800 p-6 pt-11 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<hr className="-ml-6 w-[calc(100%_+_3rem)]" />
|
||||
{children}
|
||||
<DialogPrimitive.Close className="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
<DialogPrimitive.Close className="absolute right-3 top-3 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none">
|
||||
<div className="flex gap-x-2">
|
||||
<ShortcutKey
|
||||
shortcut={{
|
||||
windows: {
|
||||
key: "esc",
|
||||
},
|
||||
mac: {
|
||||
key: "esc",
|
||||
},
|
||||
}}
|
||||
variant={"small"}
|
||||
/>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
<span className="sr-only">Close</span>
|
||||
</div>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
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 +91,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>;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function FormButtons({
|
||||
cancelButton,
|
||||
confirmButton,
|
||||
className,
|
||||
}: {
|
||||
cancelButton?: React.ReactNode;
|
||||
confirmButton: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className={cn("flex w-full items-center justify-between", className)}>
|
||||
{cancelButton ? cancelButton : <div />} {confirmButton}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function FormTitle({
|
||||
<div
|
||||
className={cn(
|
||||
"mb-4 flex flex-col gap-3 pb-4",
|
||||
divide ? "border-b border-slate-800" : "",
|
||||
divide ? "border-b border-uiBorder" : "",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,25 +22,25 @@ const HelpContext = React.createContext<HelpContextValue>({
|
||||
type HelpProps = {
|
||||
defaultOpen?: boolean;
|
||||
allowDismissing?: boolean;
|
||||
children?: React.ReactNode;
|
||||
children?: React.ReactNode | ((open: boolean) => React.ReactNode);
|
||||
};
|
||||
|
||||
export function Help({
|
||||
defaultOpen,
|
||||
allowDismissing = true,
|
||||
children,
|
||||
}: HelpProps) {
|
||||
function useHelp() {
|
||||
return React.useContext(HelpContext);
|
||||
}
|
||||
|
||||
export function Help({ defaultOpen, allowDismissing = true, children }: HelpProps) {
|
||||
const [open, setOpen] = React.useState(defaultOpen || false);
|
||||
|
||||
return (
|
||||
<HelpContext.Provider value={{ open, setOpen, allowDismissing }}>
|
||||
{children}
|
||||
{typeof children === "function" ? children(open) : children}
|
||||
</HelpContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function HelpTrigger({ title }: { title: string }) {
|
||||
const { open, setOpen } = React.useContext(HelpContext);
|
||||
const { open, setOpen } = useHelp();
|
||||
|
||||
return open ? (
|
||||
<></>
|
||||
@@ -48,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}
|
||||
@@ -65,7 +65,7 @@ export function HelpContent({
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { open, setOpen, allowDismissing } = React.useContext(HelpContext);
|
||||
const { open, setOpen, allowDismissing } = useHelp();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -89,7 +89,7 @@ export function HelpContent({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="grow rounded border border-slate-850 bg-midnight-850 bg-contain bg-left-top bg-no-repeat p-4"
|
||||
className="grow rounded border border-slate-850 bg-midnight-850 bg-contain bg-left-top bg-no-repeat p-6"
|
||||
style={{
|
||||
backgroundImage: `url(${gradientPath})`,
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph, ParagraphVariant } from "./Paragraph";
|
||||
import { Paragraph } 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;
|
||||
@@ -40,22 +37,19 @@ export function LabelValueStack({
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-baseline",
|
||||
layout === "vertical" ? "flex-col" : "gap-1",
|
||||
layout === "vertical" && "flex-col",
|
||||
variant === "primary" ? "gap-x-1 gap-y-0" : "gap-x-1 gap-y-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Paragraph variant={variation.label}>{label}</Paragraph>
|
||||
<div>
|
||||
<>
|
||||
{href ? (
|
||||
<SimpleTooltip
|
||||
side="bottom"
|
||||
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>
|
||||
@@ -66,7 +60,7 @@ export function LabelValueStack({
|
||||
) : (
|
||||
<Paragraph variant={variation.value}>{value}</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,29 +1,33 @@
|
||||
import {
|
||||
ExclamationCircleIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
StopIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowPathIcon,
|
||||
ArrowRightIcon,
|
||||
BeakerIcon,
|
||||
BellAlertIcon,
|
||||
BoltIcon,
|
||||
BookOpenIcon,
|
||||
BuildingOffice2Icon,
|
||||
CalendarDaysIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
ClockIcon,
|
||||
CloudIcon,
|
||||
CodeBracketSquareIcon,
|
||||
Cog8ToothIcon,
|
||||
CreditCardIcon,
|
||||
EnvelopeIcon,
|
||||
EyeIcon,
|
||||
FingerPrintIcon,
|
||||
FlagIcon,
|
||||
FolderIcon,
|
||||
@@ -35,146 +39,115 @@ import {
|
||||
MagnifyingGlassIcon,
|
||||
PlusIcon,
|
||||
PlusSmallIcon,
|
||||
QrCodeIcon,
|
||||
SquaresPlusIcon,
|
||||
StarIcon,
|
||||
TrashIcon,
|
||||
UserCircleIcon,
|
||||
UserGroupIcon,
|
||||
UserIcon,
|
||||
UserPlusIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
XCircleIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
|
||||
import { ActivityIcon, 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 { SaplingIcon } from "~/assets/icons/SaplingIcon";
|
||||
import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon";
|
||||
import { OneTreeIcon } from "~/assets/icons/OneTreeIcon";
|
||||
|
||||
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)} />
|
||||
),
|
||||
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)} />
|
||||
),
|
||||
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)} />
|
||||
<BellAlertIcon className={cn("text-amber-500", className)} />
|
||||
),
|
||||
docs: (className: string) => (
|
||||
<BookOpenIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
error: (className: string) => (
|
||||
<ExclamationCircleIcon className={cn("text-rose-500", 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)} />
|
||||
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)} />,
|
||||
eye: (className: string) => <EyeIcon className={cn("text-blue-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)} />
|
||||
),
|
||||
"logo-icon": (className: string) => <LogoIcon className={cn(className)} />,
|
||||
organization: (className: string) => (
|
||||
<BuildingOffice2Icon className={cn("text-fuchsia-600", className)} />
|
||||
),
|
||||
search: (className: string) => (
|
||||
<MagnifyingGlassIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
plus: (className: string) => (
|
||||
<PlusIcon className={cn("text-green-600", className)} />
|
||||
),
|
||||
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)} />
|
||||
),
|
||||
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)} />
|
||||
),
|
||||
"logo-icon": (className: string) => <LogoIcon className={cn(className)} />,
|
||||
user: (className: string) => (
|
||||
<UserIcon className={cn("text-blue-600", className)} />
|
||||
),
|
||||
property: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
|
||||
pulse: (className: string) => <ActivityIcon className={cn("text-green-600", className)} />,
|
||||
"qr-code": (className: string) => <QrCodeIcon className={cn("text-amber-400", className)} />,
|
||||
refresh: (className: string) => <ArrowPathIcon className={cn("text-bright", className)} />,
|
||||
sapling: (className: string) => <SaplingIcon className={cn("text-green-500", 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)} />,
|
||||
"trash-can": (className: string) => <TrashIcon className={cn("text-rose-500", className)} />,
|
||||
tree: (className: string) => <OneTreeIcon className={cn("text-green-500", className)} />,
|
||||
trees: (className: string) => <TwoTreesIcon className={cn("text-green-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)} />
|
||||
),
|
||||
@@ -182,8 +155,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)} />
|
||||
@@ -191,12 +167,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({
|
||||
@@ -204,7 +179,7 @@ export function NamedIcon({
|
||||
className,
|
||||
fallback,
|
||||
}: {
|
||||
name: string;
|
||||
name: IconNamesOrString;
|
||||
className: string;
|
||||
fallback?: JSX.Element;
|
||||
}) {
|
||||
@@ -214,18 +189,16 @@ 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%]"}
|
||||
variant="light"
|
||||
style={{
|
||||
shapeRendering: "geometricPrecision",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,10 +216,12 @@ export function NamedIconInBox({
|
||||
name,
|
||||
className,
|
||||
fallback,
|
||||
iconClassName,
|
||||
}: {
|
||||
name: string;
|
||||
className?: string;
|
||||
fallback?: JSX.Element;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -255,7 +230,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useContext } from "react";
|
||||
import { createContext } from "react";
|
||||
|
||||
export type OperatingSystemPlatform = "mac" | "windows";
|
||||
|
||||
type OperatingSystemContext = {
|
||||
platform: OperatingSystemPlatform;
|
||||
};
|
||||
|
||||
type OperatingSystemContextProviderProps = {
|
||||
platform: OperatingSystemPlatform;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const Context = createContext<OperatingSystemContext | null>(null);
|
||||
|
||||
export const OperatingSystemContextProvider = ({
|
||||
platform,
|
||||
children,
|
||||
}: OperatingSystemContextProviderProps) => {
|
||||
return <Context.Provider value={{ platform }}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
const throwIfNoProvider = () => {
|
||||
throw new Error("Please wrap your application in an OperatingSystemContextProvider.");
|
||||
};
|
||||
|
||||
export const useOperatingSystem = () => {
|
||||
const { platform } = useContext(Context) ?? throwIfNoProvider();
|
||||
return { platform };
|
||||
};
|
||||
@@ -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-uiBorder 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>
|
||||
@@ -65,7 +54,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
|
||||
}
|
||||
|
||||
export function PageButtons({ children }: WithChildren) {
|
||||
return <div className="flex gap-2">{children}</div>;
|
||||
return <div className="flex items-center gap-3">{children}</div>;
|
||||
}
|
||||
|
||||
export function PageDescription({ children }: WithChildren) {
|
||||
@@ -102,27 +91,18 @@ export function PageInfoProperty({
|
||||
value,
|
||||
}: {
|
||||
icon?: string | React.ReactNode;
|
||||
label: string;
|
||||
label?: string;
|
||||
value: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{icon && typeof icon === "string" ? (
|
||||
<NamedIcon name={icon} className="h-4 w-4" />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
<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
|
||||
{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">
|
||||
{label}:
|
||||
</Paragraph>
|
||||
)}
|
||||
<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