Compare commits
949 Commits
@nhost/goo
...
@nhost/apo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b649f178e0 | ||
|
|
1f5e1e3d42 | ||
|
|
5727b0b0fe | ||
|
|
2f38ed56f5 | ||
|
|
16502ea175 | ||
|
|
beee0407df | ||
|
|
3990b1ffbb | ||
|
|
1fb03708e3 | ||
|
|
d42719ee65 | ||
|
|
72ff489ea8 | ||
|
|
c9bf2dde0e | ||
|
|
613533d377 | ||
|
|
8568354718 | ||
|
|
1be6d32455 | ||
|
|
812a6e5005 | ||
|
|
34cc230b61 | ||
|
|
898a7c835f | ||
|
|
7766624bc5 | ||
|
|
2e8f73df38 | ||
|
|
6a419e060e | ||
|
|
43480ca735 | ||
|
|
efc42d77fd | ||
|
|
31e2523eca | ||
|
|
fbf4f40ab7 | ||
|
|
cbe203e720 | ||
|
|
378a6684b0 | ||
|
|
1999ae09e6 | ||
|
|
0fe48a0833 | ||
|
|
7bbf6dbf1c | ||
|
|
689dc873b3 | ||
|
|
a0747d02e0 | ||
|
|
be5bd1e446 | ||
|
|
52ccfdec89 | ||
|
|
2c60591580 | ||
|
|
6140bc5b3b | ||
|
|
9f7780ec91 | ||
|
|
abc7d0c7a5 | ||
|
|
074a36ea48 | ||
|
|
64e806dc27 | ||
|
|
bd0e9748b6 | ||
|
|
b21222b378 | ||
|
|
7e217db128 | ||
|
|
56c716d9fa | ||
|
|
14ecbd1fb9 | ||
|
|
a0242c4d6f | ||
|
|
4800b4a756 | ||
|
|
5b318d17d4 | ||
|
|
2f9be4f760 | ||
|
|
64777b6f30 | ||
|
|
7e1489353e | ||
|
|
c53306a497 | ||
|
|
83345579d0 | ||
|
|
2b4b9e0385 | ||
|
|
922349f550 | ||
|
|
d613f3fd04 | ||
|
|
4d8a47777e | ||
|
|
229a7ab1f7 | ||
|
|
3dabb7b53a | ||
|
|
abc3d6ce60 | ||
|
|
08d49bd1fd | ||
|
|
03435a2c66 | ||
|
|
66208d6840 | ||
|
|
5be9abb0fa | ||
|
|
8e504b5328 | ||
|
|
0e3eb7204a | ||
|
|
70cfeb1fcf | ||
|
|
3116562b58 | ||
|
|
693e40d385 | ||
|
|
9a1aa7bb2e | ||
|
|
98345f2e78 | ||
|
|
f29abe6238 | ||
|
|
8956d47bce | ||
|
|
dd0738d5f7 | ||
|
|
11d77d6011 | ||
|
|
a78cd2f18f | ||
|
|
6ef340daad | ||
|
|
a96e3c9163 | ||
|
|
229c47cf16 | ||
|
|
e5e705350d | ||
|
|
4f81b0695d | ||
|
|
9c5b6532d3 | ||
|
|
889df8ca4d | ||
|
|
b998e09e10 | ||
|
|
9e0486a362 | ||
|
|
74037cec68 | ||
|
|
800db1b300 | ||
|
|
a40baa8c63 | ||
|
|
5cc06609c2 | ||
|
|
819e68b501 | ||
|
|
efa68aab83 | ||
|
|
3a696d366a | ||
|
|
e3e21b6164 | ||
|
|
9259663c76 | ||
|
|
26dd7faf05 | ||
|
|
10cc213933 | ||
|
|
1b5cb93761 | ||
|
|
4157c012fd | ||
|
|
9515096349 | ||
|
|
4dd5617855 | ||
|
|
01dc358842 | ||
|
|
925a1808e6 | ||
|
|
ac80f88727 | ||
|
|
3078247629 | ||
|
|
144c0084d2 | ||
|
|
bbdfb77a07 | ||
|
|
b6df9e2e8c | ||
|
|
48f15eb849 | ||
|
|
141642d40d | ||
|
|
def4a3a2ea | ||
|
|
b5a9c1be47 | ||
|
|
5b5e7d9640 | ||
|
|
b876a4ada1 | ||
|
|
c02e0c63f2 | ||
|
|
7e064355ba | ||
|
|
788482fab2 | ||
|
|
16d94821b8 | ||
|
|
0f6ece6b8c | ||
|
|
5c4ab54c90 | ||
|
|
793e7392da | ||
|
|
5941568bbb | ||
|
|
1a9e1fde1d | ||
|
|
a9bbd1303e | ||
|
|
b0ed2b6f14 | ||
|
|
3e951eab4f | ||
|
|
cd7a198715 | ||
|
|
7c4d05a25e | ||
|
|
32c0632526 | ||
|
|
19cca7f45d | ||
|
|
191580a819 | ||
|
|
4a57861354 | ||
|
|
d96b817476 | ||
|
|
db6db8d860 | ||
|
|
fe405ba123 | ||
|
|
972a5f652f | ||
|
|
7a2c140524 | ||
|
|
3d717d68a9 | ||
|
|
13b6a47bef | ||
|
|
2a6caa47bd | ||
|
|
dbbfaef451 | ||
|
|
b4c07f1723 | ||
|
|
e983eb53d9 | ||
|
|
49867fdcf7 | ||
|
|
cc428d73ee | ||
|
|
940a1db0fc | ||
|
|
d70f29a408 | ||
|
|
52cb055520 | ||
|
|
8c406237a2 | ||
|
|
d036e282e5 | ||
|
|
39530cd8e8 | ||
|
|
8b58627608 | ||
|
|
c4e2d87e5c | ||
|
|
66b0378d38 | ||
|
|
22d7a36247 | ||
|
|
7d2eb2de66 | ||
|
|
4bba002c30 | ||
|
|
881a3344d4 | ||
|
|
d1562d33fb | ||
|
|
21ced66f22 | ||
|
|
2a2d86904d | ||
|
|
82d46f716b | ||
|
|
5e26810868 | ||
|
|
d590258371 | ||
|
|
697ef57cb8 | ||
|
|
93002dc8c3 | ||
|
|
357e0933ff | ||
|
|
baa1937d06 | ||
|
|
03e5662df9 | ||
|
|
e0711bdfc8 | ||
|
|
caca27fde3 | ||
|
|
b0d51033c6 | ||
|
|
088f9394fc | ||
|
|
a91361f971 | ||
|
|
a4f5be6ab9 | ||
|
|
dbbccbf1cd | ||
|
|
7d8f82b99d | ||
|
|
a70dc7b352 | ||
|
|
54df0df42b | ||
|
|
0bbb2598fd | ||
|
|
e10480b761 | ||
|
|
1343abbe50 | ||
|
|
4ba34cc827 | ||
|
|
fa37546139 | ||
|
|
42ece48ce3 | ||
|
|
112526a984 | ||
|
|
f97ab31f69 | ||
|
|
21dc1ecd6b | ||
|
|
c11adbe3e2 | ||
|
|
6078e9c207 | ||
|
|
450582dc43 | ||
|
|
ddec0e1be1 | ||
|
|
698154b24b | ||
|
|
9c87b0f67b | ||
|
|
85afe3d216 | ||
|
|
0773e5215f | ||
|
|
10f25fcc4e | ||
|
|
2ee90d6ea3 | ||
|
|
5db5323a1d | ||
|
|
0c74806245 | ||
|
|
5df84d7f50 | ||
|
|
d58de8fcf8 | ||
|
|
ed93d4b583 | ||
|
|
1ec1953eaa | ||
|
|
63e9c3933e | ||
|
|
85674c4d90 | ||
|
|
5a1d3b9bfc | ||
|
|
942570ed29 | ||
|
|
1b1620f633 | ||
|
|
e8e8d661e1 | ||
|
|
ba08ec7f5c | ||
|
|
fb0c98c21d | ||
|
|
c9f575c40c | ||
|
|
6c8bed7ecc | ||
|
|
3058eee48f | ||
|
|
d5a712f7ef | ||
|
|
83422f5ee6 | ||
|
|
51909a6a8f | ||
|
|
2f30797556 | ||
|
|
0b8f7d1661 | ||
|
|
52ee9d84b6 | ||
|
|
d9612b28b0 | ||
|
|
0034791493 | ||
|
|
80fed14a6b | ||
|
|
d457ada435 | ||
|
|
b41e5a9df5 | ||
|
|
0c8ace1bd4 | ||
|
|
3f800a068b | ||
|
|
7d490fe569 | ||
|
|
d6527122db | ||
|
|
3211140dec | ||
|
|
469352cd81 | ||
|
|
88400f6b7c | ||
|
|
f8c8a06d71 | ||
|
|
ebc1730fce | ||
|
|
c1cd1e813c | ||
|
|
e08a074474 | ||
|
|
2f819865bc | ||
|
|
3888f3041f | ||
|
|
bacb1b9720 | ||
|
|
e119e4fc18 | ||
|
|
569c4004f6 | ||
|
|
95932fa3f2 | ||
|
|
99402b77d1 | ||
|
|
f6fb2cd8e6 | ||
|
|
5c2cf59b41 | ||
|
|
a6d31dc260 | ||
|
|
b1fe2be963 | ||
|
|
872e50b635 | ||
|
|
bd73557a47 | ||
|
|
9b6e8ab3bc | ||
|
|
c95bab70c2 | ||
|
|
52d4b5de45 | ||
|
|
fe0742e278 | ||
|
|
ded57d3b24 | ||
|
|
c30abaea22 | ||
|
|
6b4ab50f74 | ||
|
|
ceba605d0b | ||
|
|
9249a85ee5 | ||
|
|
d2c4b7cad1 | ||
|
|
59d737696a | ||
|
|
22de3214f1 | ||
|
|
cf880f992f | ||
|
|
f4d70f88e9 | ||
|
|
0d09b80b12 | ||
|
|
195adfb04a | ||
|
|
aee4cdcb72 | ||
|
|
b09930c8a4 | ||
|
|
687951281e | ||
|
|
c0f05acd9b | ||
|
|
87af60cc03 | ||
|
|
3d8dd39995 | ||
|
|
65687beecc | ||
|
|
62aa859737 | ||
|
|
d8c2d369aa | ||
|
|
a4e4926aeb | ||
|
|
35cd76e562 | ||
|
|
266bbe837d | ||
|
|
caf785a938 | ||
|
|
9bc3e755df | ||
|
|
4a9471cc16 | ||
|
|
638a7ac11d | ||
|
|
567e370bdc | ||
|
|
a91f2db0e2 | ||
|
|
4e49c8db50 | ||
|
|
210f65b4db | ||
|
|
1b6482126f | ||
|
|
96f9c1a55d | ||
|
|
731460b20d | ||
|
|
1537d46b1d | ||
|
|
632def158d | ||
|
|
39271a67e2 | ||
|
|
9e25c4f386 | ||
|
|
dd58a4ac7f | ||
|
|
b9c3567baa | ||
|
|
108937789a | ||
|
|
e651745a7e | ||
|
|
699debb2b8 | ||
|
|
3e08dc7f8c | ||
|
|
6928b48781 | ||
|
|
02886350ff | ||
|
|
b3672f8246 | ||
|
|
6091b4a8e8 | ||
|
|
aea99ad2c8 | ||
|
|
594488e435 | ||
|
|
bb83b0f81a | ||
|
|
82ddcbd180 | ||
|
|
8aa7aafa3b | ||
|
|
183cb4b26a | ||
|
|
3a7377c6e2 | ||
|
|
1529f58c33 | ||
|
|
95af5421d1 | ||
|
|
feb39404db | ||
|
|
0384d7c7c4 | ||
|
|
7e356a9604 | ||
|
|
013e55a307 | ||
|
|
2a71257cde | ||
|
|
583a4401d0 | ||
|
|
914e91a0b0 | ||
|
|
98698213e2 | ||
|
|
756daa97cd | ||
|
|
ab5a2b119c | ||
|
|
ffdcce1463 | ||
|
|
15b3100c63 | ||
|
|
f7ef7d106d | ||
|
|
0dbbcc5595 | ||
|
|
a3dcb6106e | ||
|
|
208d224763 | ||
|
|
816f8d069d | ||
|
|
72c31622cd | ||
|
|
6959461e3f | ||
|
|
103472ac77 | ||
|
|
2ebf99ff8f | ||
|
|
c13e492bbf | ||
|
|
564d000bfc | ||
|
|
63476a2351 | ||
|
|
266fda07ab | ||
|
|
782252c059 | ||
|
|
e86978a1ff | ||
|
|
84cfd11953 | ||
|
|
9a43e136f6 | ||
|
|
e9cff26fa0 | ||
|
|
3d32bca2b3 | ||
|
|
4021feaf38 | ||
|
|
6174e1ddcc | ||
|
|
af2e3eae37 | ||
|
|
d2b4c126f3 | ||
|
|
7f2e182c47 | ||
|
|
306ec74356 | ||
|
|
ae40bd54d4 | ||
|
|
b5cc47078a | ||
|
|
7f251111e2 | ||
|
|
c03dacc3a3 | ||
|
|
8b9e1a0ce8 | ||
|
|
cf9cfec330 | ||
|
|
1c94f56c59 | ||
|
|
f06d5deba3 | ||
|
|
8ff58d7f23 | ||
|
|
8dd1c7415b | ||
|
|
ebd2749e38 | ||
|
|
80b604adda | ||
|
|
9d73050792 | ||
|
|
2a86b8876c | ||
|
|
91a1a41f5d | ||
|
|
22e9c27c81 | ||
|
|
2d2beb53d2 | ||
|
|
b403b0d6a0 | ||
|
|
4dbac55cb4 | ||
|
|
c6e31ac741 | ||
|
|
0d3e8b3992 | ||
|
|
b2afd14d61 | ||
|
|
f28f28b6ee | ||
|
|
834b959271 | ||
|
|
4dbc9ccc87 | ||
|
|
2764a1c4b6 | ||
|
|
1666ca2ec5 | ||
|
|
346791d4d5 | ||
|
|
94bdafe22f | ||
|
|
33782e9d41 | ||
|
|
ea02e1e104 | ||
|
|
98bf6e3792 | ||
|
|
d9dcafd643 | ||
|
|
4f3d97b5ad | ||
|
|
d1801ceae9 | ||
|
|
b6f9fe6304 | ||
|
|
15b652d7e0 | ||
|
|
cdc0047cb7 | ||
|
|
7c0e71e8be | ||
|
|
175d6d8cbc | ||
|
|
2ae277409a | ||
|
|
ada10170b7 | ||
|
|
0ed77cbe8b | ||
|
|
1abc68992f | ||
|
|
795962e3c2 | ||
|
|
ba998eb632 | ||
|
|
d3fc1bbeb9 | ||
|
|
7b9c2016d0 | ||
|
|
ebf4070be6 | ||
|
|
59e3f6abc6 | ||
|
|
0c9a03a7ff | ||
|
|
42cbe27914 | ||
|
|
d7e7b0e51b | ||
|
|
036181dd75 | ||
|
|
2828a9fe01 | ||
|
|
88bc71dffc | ||
|
|
17699870a6 | ||
|
|
5c0304ab73 | ||
|
|
637265e3d9 | ||
|
|
229604d8e1 | ||
|
|
1165a33079 | ||
|
|
82c0dd9d87 | ||
|
|
a9bfab1778 | ||
|
|
60b7a664d2 | ||
|
|
4f708d04d6 | ||
|
|
a02af56056 | ||
|
|
575136dcfb | ||
|
|
2bc6346cbc | ||
|
|
91cd494a3d | ||
|
|
d57e0a5287 | ||
|
|
dd5e7093f0 | ||
|
|
3e9bb84f07 | ||
|
|
6665b58ec8 | ||
|
|
456e893cd6 | ||
|
|
fd76170ca6 | ||
|
|
682aef2d94 | ||
|
|
cb53f71d4a | ||
|
|
4fc0b40cb4 | ||
|
|
9da2d01c55 | ||
|
|
0d5e7850f8 | ||
|
|
8ed965c669 | ||
|
|
cb284b40b1 | ||
|
|
c59f622feb | ||
|
|
9a30edd038 | ||
|
|
4b2d2a4f55 | ||
|
|
eba2bd05b8 | ||
|
|
84a1b28261 | ||
|
|
ff3b9df41e | ||
|
|
b8b4e36175 | ||
|
|
6e47ef68d5 | ||
|
|
2697414637 | ||
|
|
e753b2faed | ||
|
|
6f62ec7d2a | ||
|
|
d365ef1953 | ||
|
|
99ee9fd10d | ||
|
|
9608a327c9 | ||
|
|
492b83ef58 | ||
|
|
09f53ae43f | ||
|
|
7724ac7e06 | ||
|
|
02343fe171 | ||
|
|
98dd8d039c | ||
|
|
456e057497 | ||
|
|
93e9b58a58 | ||
|
|
019a7c2335 | ||
|
|
a0901914ac | ||
|
|
64882a8e16 | ||
|
|
1ed572fe39 | ||
|
|
e06271a8ae | ||
|
|
bca239ebb2 | ||
|
|
e33caa046d | ||
|
|
cbbd331341 | ||
|
|
c41bfaffdd | ||
|
|
f2596b0b14 | ||
|
|
4d0c3111d1 | ||
|
|
7ff9644ac7 | ||
|
|
87fdaa7144 | ||
|
|
bc23d051ba | ||
|
|
132a4f4be9 | ||
|
|
c330dc1c00 | ||
|
|
4bebd9842b | ||
|
|
541f2d250c | ||
|
|
6b31e31430 | ||
|
|
9ba2208dd7 | ||
|
|
538541fc79 | ||
|
|
cb0aab48b3 | ||
|
|
dcc760b6bb | ||
|
|
d2227be0d0 | ||
|
|
93e807edc4 | ||
|
|
14738d4b58 | ||
|
|
6ac4cfa80c | ||
|
|
295c6c9110 | ||
|
|
02911e4a54 | ||
|
|
2be825de08 | ||
|
|
65afad1b91 | ||
|
|
dbea13a1d2 | ||
|
|
5e2a419700 | ||
|
|
f1272947dd | ||
|
|
e5041bfd30 | ||
|
|
3d7cc74feb | ||
|
|
d007202783 | ||
|
|
38e92a705d | ||
|
|
9e6660450c | ||
|
|
2db9be03ff | ||
|
|
1b8f505050 | ||
|
|
6ab6bccded | ||
|
|
b12bc1c27f | ||
|
|
04d1641cf7 | ||
|
|
e91c09aa9c | ||
|
|
7c04aad870 | ||
|
|
520044c5ab | ||
|
|
22fed668d6 | ||
|
|
ae14fb1a2f | ||
|
|
74cc63833a | ||
|
|
f4ca0aab42 | ||
|
|
d8a0d196af | ||
|
|
cd53718631 | ||
|
|
1fcd3f1851 | ||
|
|
67d370feaa | ||
|
|
0173ec356b | ||
|
|
2fb43d417a | ||
|
|
966b8158c2 | ||
|
|
800fac6093 | ||
|
|
20eb04cd46 | ||
|
|
425320bbb5 | ||
|
|
499352ad8a | ||
|
|
5d31a88556 | ||
|
|
06019ae7ba | ||
|
|
523af2901f | ||
|
|
1e0a2c0c43 | ||
|
|
9b60f9ca48 | ||
|
|
90bc800c92 | ||
|
|
023060cee6 | ||
|
|
0bb3b127d0 | ||
|
|
cd914d30ed | ||
|
|
71dddccb9c | ||
|
|
2a846040ef | ||
|
|
8b580d05c4 | ||
|
|
24d70e8ea8 | ||
|
|
8212432983 | ||
|
|
7df00a3291 | ||
|
|
0186aa5c98 | ||
|
|
67e5cc839c | ||
|
|
25c5bd1ccd | ||
|
|
e5fa48d818 | ||
|
|
f34da3a715 | ||
|
|
fab330ce55 | ||
|
|
3a76080e40 | ||
|
|
db57572f38 | ||
|
|
b030eae999 | ||
|
|
b10f4fa631 | ||
|
|
4a6e8c9945 | ||
|
|
3a4d087e6f | ||
|
|
c408b38f28 | ||
|
|
b2336b2b33 | ||
|
|
fcf68d5c32 | ||
|
|
9e71677cd2 | ||
|
|
ce48ce0860 | ||
|
|
9b93cf95ec | ||
|
|
34d85e54d6 | ||
|
|
6ae9d6dd01 | ||
|
|
1e8d8afe70 | ||
|
|
80478e39e0 | ||
|
|
ee7338cc12 | ||
|
|
3a39970d28 | ||
|
|
b0607673ed | ||
|
|
d9f27c6c65 | ||
|
|
ea0b35fe84 | ||
|
|
4fbd22f232 | ||
|
|
c92142845f | ||
|
|
c93f818315 | ||
|
|
d20ed86219 | ||
|
|
e04390307c | ||
|
|
5c931f14ae | ||
|
|
a7ab1243a4 | ||
|
|
5638b5e770 | ||
|
|
c5ce895c97 | ||
|
|
2be99c0e55 | ||
|
|
f6299b537d | ||
|
|
11d06cce4b | ||
|
|
e9e0ad6d3c | ||
|
|
3ad2d5ced8 | ||
|
|
42bfa7c16d | ||
|
|
0dea974a2e | ||
|
|
8165b8f13e | ||
|
|
1c8864c992 | ||
|
|
dd6921676f | ||
|
|
a1193da4fb | ||
|
|
ff83a29dcc | ||
|
|
4771bff65c | ||
|
|
6106265d87 | ||
|
|
3c2455b7e1 | ||
|
|
7bc8de103b | ||
|
|
e49ba4bd02 | ||
|
|
44ac1a0095 | ||
|
|
04ce686e7c | ||
|
|
ca01224083 | ||
|
|
d489232cbc | ||
|
|
9efc0522c3 | ||
|
|
4c86ed787e | ||
|
|
9a3c05f69b | ||
|
|
2b03f80d59 | ||
|
|
baa3f0531d | ||
|
|
f9bbc78241 | ||
|
|
7f62c8b7c2 | ||
|
|
de2bcb6320 | ||
|
|
64a6d934e7 | ||
|
|
f455022e6d | ||
|
|
baac8a3605 | ||
|
|
be20cd5cdb | ||
|
|
1de4137369 | ||
|
|
18fae1a891 | ||
|
|
03c58613b8 | ||
|
|
fb23dd6b39 | ||
|
|
c12bd632fc | ||
|
|
a71eea09a9 | ||
|
|
c9f88326b2 | ||
|
|
d1f7842169 | ||
|
|
4566c2a202 | ||
|
|
c678918e73 | ||
|
|
ac8efcbdd5 | ||
|
|
9bc346e8d4 | ||
|
|
efed987d31 | ||
|
|
7e113bfb1f | ||
|
|
bdab7da7d3 | ||
|
|
c2d9993968 | ||
|
|
508ba62207 | ||
|
|
a3318de06e | ||
|
|
f1d358d77c | ||
|
|
d558ef9ecf | ||
|
|
26d577d7ae | ||
|
|
75c7ba7f12 | ||
|
|
d62dfe19a2 | ||
|
|
0dbef188b1 | ||
|
|
fa9f7ca052 | ||
|
|
8857314e22 | ||
|
|
85f1c4a98e | ||
|
|
efa6b5755d | ||
|
|
44f13f6240 | ||
|
|
2b19416787 | ||
|
|
e01cb2ed49 | ||
|
|
388eef041f | ||
|
|
4e5d43f300 | ||
|
|
622c48a94b | ||
|
|
e1a87a05b1 | ||
|
|
db342f453e | ||
|
|
2148317282 | ||
|
|
54386a3b56 | ||
|
|
ff40b99f84 | ||
|
|
33f8f1d78a | ||
|
|
c50fe47ab4 | ||
|
|
0580f832c8 | ||
|
|
7d1eb099c0 | ||
|
|
e15322296b | ||
|
|
91a2bf905b | ||
|
|
0f9393fe27 | ||
|
|
aebb822549 | ||
|
|
1e2be6fadf | ||
|
|
aafbf5173d | ||
|
|
01e13e2f8c | ||
|
|
4364647501 | ||
|
|
ef117c284e | ||
|
|
3f919c0a80 | ||
|
|
49e447e7b7 | ||
|
|
66b4f3d0be | ||
|
|
aa7fdafe8b | ||
|
|
5f9c6c8346 | ||
|
|
7d6de3b289 | ||
|
|
57e41f77a9 | ||
|
|
f5c2a0ef4f | ||
|
|
2e30371086 | ||
|
|
d52bc8cca5 | ||
|
|
04a3e4c965 | ||
|
|
853c0c5775 | ||
|
|
2e6923dc73 | ||
|
|
7d6d70d0c7 | ||
|
|
57db5b83d4 | ||
|
|
7a2100cc17 | ||
|
|
5d55f3fa60 | ||
|
|
8b0c44a93c | ||
|
|
e0cc7cce0a | ||
|
|
6e7d5e0dd4 | ||
|
|
54c143ebf6 | ||
|
|
8b9fa0b150 | ||
|
|
c3bb79e1dd | ||
|
|
128d21e4ec | ||
|
|
40e503c356 | ||
|
|
d007e0ade8 | ||
|
|
fa32513ba7 | ||
|
|
8893d9e010 | ||
|
|
66659bb293 | ||
|
|
579d4f3170 | ||
|
|
bb56548603 | ||
|
|
81d2fd865c | ||
|
|
fe3c462099 | ||
|
|
f8b082cb02 | ||
|
|
0c748e6ee6 | ||
|
|
e2c4ca85b3 | ||
|
|
0165b998c2 | ||
|
|
5d970cc229 | ||
|
|
7167170663 | ||
|
|
0f77de2dd0 | ||
|
|
6ae91e48d1 | ||
|
|
69db1594cc | ||
|
|
158cf0da49 | ||
|
|
7992fc3baa | ||
|
|
85d9596956 | ||
|
|
16d383516e | ||
|
|
2ca193ccf3 | ||
|
|
ab8e12003d | ||
|
|
29cdf6b125 | ||
|
|
41cc3dc5d0 | ||
|
|
6b67c9996a | ||
|
|
23274dee41 | ||
|
|
a5b55c2667 | ||
|
|
1263676eb3 | ||
|
|
b1b647ad96 | ||
|
|
21bbaf5e95 | ||
|
|
eef9c91403 | ||
|
|
1742cb444d | ||
|
|
c4f374d7f3 | ||
|
|
369ec13070 | ||
|
|
101129eef2 | ||
|
|
228fda0364 | ||
|
|
74085c67a2 | ||
|
|
a273725419 | ||
|
|
c5240f8d74 | ||
|
|
4490068257 | ||
|
|
3601de3f85 | ||
|
|
ac9404610b | ||
|
|
63570db57c | ||
|
|
538ed78f5a | ||
|
|
b1a31ecb00 | ||
|
|
3d151c448c | ||
|
|
bac8ace434 | ||
|
|
fdd417ed25 | ||
|
|
3cab18713a | ||
|
|
a402fc17de | ||
|
|
fb94dae43a | ||
|
|
4416ceb9cf | ||
|
|
f694846eae | ||
|
|
4762ebf61e | ||
|
|
73e28b5831 | ||
|
|
2a7dc5060f | ||
|
|
9b8ede40a9 | ||
|
|
f005c20d99 | ||
|
|
4adfd613b6 | ||
|
|
b6da82c8e3 | ||
|
|
816456edc4 | ||
|
|
deaf0e86d4 | ||
|
|
23f8206f18 | ||
|
|
9dde4d7988 | ||
|
|
26385b9cf9 | ||
|
|
6d318206ef | ||
|
|
4d727b78a1 | ||
|
|
de0a125e98 | ||
|
|
ea1ad29031 | ||
|
|
3da40e5712 | ||
|
|
b9087a4add | ||
|
|
1b7a6d0252 | ||
|
|
1417d3e794 | ||
|
|
e187923858 | ||
|
|
8a60ed4074 | ||
|
|
d7d11a44a7 | ||
|
|
062e4691cd | ||
|
|
a95d49fa2c | ||
|
|
d14fc96899 | ||
|
|
93db718254 | ||
|
|
c367bd58b9 | ||
|
|
0bfed4d9e1 | ||
|
|
1f3aecd379 | ||
|
|
42306ea3bb | ||
|
|
1b12a175f6 | ||
|
|
32060aaea0 | ||
|
|
f94cace5f2 | ||
|
|
5de965d9a5 | ||
|
|
e10b3adc11 | ||
|
|
457db76b06 | ||
|
|
1e952a026e | ||
|
|
2f4c040789 | ||
|
|
74648752b4 | ||
|
|
09d218a3fe | ||
|
|
2e8938dbb0 | ||
|
|
ec60d03536 | ||
|
|
2f3767552f | ||
|
|
bc401c0dd2 | ||
|
|
2907ecb7ff | ||
|
|
05d7f5207f | ||
|
|
07a053ee80 | ||
|
|
2145243b19 | ||
|
|
61e4414a8f | ||
|
|
4601d84e0e | ||
|
|
ca012d790c | ||
|
|
aeda14ef53 | ||
|
|
3fa5e2005a | ||
|
|
4dd2e99159 | ||
|
|
282c6c6d24 | ||
|
|
beadd84adb | ||
|
|
f8f55d2b99 | ||
|
|
03a98d4f3a | ||
|
|
8ed8e04ab6 | ||
|
|
587efd4551 | ||
|
|
c78227b085 | ||
|
|
d87e520307 | ||
|
|
bbed04e4da | ||
|
|
273afc9740 | ||
|
|
f4083aa4b3 | ||
|
|
ddd2641726 | ||
|
|
4658aeb31e | ||
|
|
cc8e5fe4a9 | ||
|
|
85c897c717 | ||
|
|
c99e5552e6 | ||
|
|
97a2520ea1 | ||
|
|
964af2912b | ||
|
|
a48dd5bf74 | ||
|
|
ef53df5cb3 | ||
|
|
afea682a8c | ||
|
|
fefa2baa2e | ||
|
|
f09b3cfd24 | ||
|
|
dd3b2c41f1 | ||
|
|
aaced20f31 | ||
|
|
3e91c19e13 | ||
|
|
abe0edcacb | ||
|
|
f8dae56bda | ||
|
|
9133726dbe | ||
|
|
7eed617034 | ||
|
|
d4fd4ec3e9 | ||
|
|
19a0288861 | ||
|
|
da975387ac | ||
|
|
e46c77e409 | ||
|
|
6c642d86f3 | ||
|
|
46a77f1ce5 | ||
|
|
6053560b5a | ||
|
|
89bd37bc28 | ||
|
|
0df73a41c9 | ||
|
|
322ab50138 | ||
|
|
53bdc294e2 | ||
|
|
f6d2042adb | ||
|
|
ba83475ced | ||
|
|
dafc581c08 | ||
|
|
c88b77ef43 | ||
|
|
1470592aac | ||
|
|
4e9a560346 | ||
|
|
766cb61243 | ||
|
|
7a9370abb2 | ||
|
|
73368c87a2 | ||
|
|
ef20f1f504 | ||
|
|
616a71fc89 | ||
|
|
9477e11d4c | ||
|
|
3c0adb4922 | ||
|
|
c1bfc16ec2 | ||
|
|
1fe86f770c | ||
|
|
d5de56256a | ||
|
|
b5e8222b76 | ||
|
|
7f15375a9a | ||
|
|
ffd8660bcc | ||
|
|
9159cf46b1 | ||
|
|
9211743d9c | ||
|
|
cc6aae3fba | ||
|
|
a9fbe8e0fc | ||
|
|
40cbeac221 | ||
|
|
df8e31305d | ||
|
|
90af9f2224 | ||
|
|
037fbdf37a | ||
|
|
843087cb11 | ||
|
|
435efd2bc5 | ||
|
|
f2aaff0504 | ||
|
|
ee2f53a052 | ||
|
|
8f5255172e | ||
|
|
c9de90e027 | ||
|
|
3341632f23 | ||
|
|
e005a67ab4 | ||
|
|
1f4bbf75e0 | ||
|
|
e5934d5dfd | ||
|
|
8b368ba2e8 | ||
|
|
16017fb8d2 | ||
|
|
effc0aba52 | ||
|
|
45a81ca823 | ||
|
|
377e8f8c37 | ||
|
|
977d58a938 | ||
|
|
eb7a14cedb | ||
|
|
5a64bdf30a | ||
|
|
7acf96d65f | ||
|
|
57726864fd | ||
|
|
73da6a67f1 | ||
|
|
48964b82e0 | ||
|
|
9d2fdbadc8 | ||
|
|
6bd874b485 | ||
|
|
4b36670897 | ||
|
|
947696efc6 | ||
|
|
a3168a1dae | ||
|
|
29efea2ad8 | ||
|
|
390688feb1 | ||
|
|
20e19ec7db | ||
|
|
b0c58ff351 | ||
|
|
7b7cc74948 | ||
|
|
1f501c829c | ||
|
|
8f9993d8ed | ||
|
|
f53b1f5c13 | ||
|
|
1b8dcf237a | ||
|
|
fc559d9e29 | ||
|
|
fe61dbb6dc | ||
|
|
8f90569230 | ||
|
|
5a11ace8f0 | ||
|
|
c569c5f60c | ||
|
|
fbcef432a3 | ||
|
|
44ae629f86 | ||
|
|
e030856660 | ||
|
|
db118f9769 | ||
|
|
8a48a897a7 | ||
|
|
dce91ec7d8 | ||
|
|
0a3383d6c5 | ||
|
|
97b5310c5d | ||
|
|
5c8c79444a | ||
|
|
eb3041341d | ||
|
|
f57f237e37 | ||
|
|
b1e90e6e2b | ||
|
|
1c947b2995 | ||
|
|
5501a5937e | ||
|
|
d00f6ed84e | ||
|
|
4c88846d72 | ||
|
|
80a6808a82 | ||
|
|
97dc689d79 | ||
|
|
311417d679 | ||
|
|
ce0e1ee7ae | ||
|
|
1cc6841107 | ||
|
|
4b7fff0440 | ||
|
|
47fc7ffc0e | ||
|
|
7c5d0d0ec6 | ||
|
|
4d9c48f524 | ||
|
|
987bd70312 | ||
|
|
842e9892c0 | ||
|
|
37fee16552 | ||
|
|
feb22e62c1 | ||
|
|
e7d4c77a6d | ||
|
|
628e32464d | ||
|
|
d056fb4dbd | ||
|
|
ed6d9e8a85 | ||
|
|
7840201e91 | ||
|
|
af8891686b | ||
|
|
13efafb000 | ||
|
|
719a3ddcf9 | ||
|
|
d11980f078 | ||
|
|
bfbe8733f6 | ||
|
|
dc82043254 | ||
|
|
997e9d58a8 | ||
|
|
8a3f1706fe | ||
|
|
12cbe4d534 | ||
|
|
9f21931201 | ||
|
|
09351e1910 | ||
|
|
1c2ea5a407 | ||
|
|
7055ffc37a | ||
|
|
c68ce6d480 | ||
|
|
412b1fa8c6 | ||
|
|
98a149c8bf | ||
|
|
ceb558975e | ||
|
|
7a87321a7e | ||
|
|
9349766c0a | ||
|
|
31655191a3 | ||
|
|
e3b91efa84 | ||
|
|
cfe736776a | ||
|
|
481bf237cc | ||
|
|
33ce9bf1b9 |
@@ -2,9 +2,22 @@
|
||||
"$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"linked": [],
|
||||
"linked": [
|
||||
[
|
||||
"@nhost/nextjs",
|
||||
"@nhost/react",
|
||||
"@nhost/vue",
|
||||
"@nhost/nhost-js",
|
||||
"@nhost/hasura-auth-js",
|
||||
"@nhost/hasura-storage-js"
|
||||
],
|
||||
[
|
||||
"@nhost/react-apollo",
|
||||
"@nhost/apollo"
|
||||
]
|
||||
],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
}
|
||||
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/out
|
||||
**/dist
|
||||
**/umd
|
||||
**/.turbo
|
||||
**/.nhost
|
||||
**/coverage
|
||||
**/.next
|
||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
22
.github/actions/install-dependencies/action.yaml
vendored
22
.github/actions/install-dependencies/action.yaml
vendored
@@ -1,11 +1,20 @@
|
||||
name: Install Node and package dependencies
|
||||
description: 'Install Node dependencies with pnpm'
|
||||
inputs:
|
||||
TURBO_TOKEN:
|
||||
description: 'Turborepo token'
|
||||
TURBO_TEAM:
|
||||
description: 'Turborepo team'
|
||||
BUILD:
|
||||
description: 'Build packages'
|
||||
default: 'default'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.9.1
|
||||
version: 7.17.0
|
||||
run_install: false
|
||||
- name: Get pnpm cache directory
|
||||
id: pnpm-cache-dir
|
||||
@@ -30,4 +39,15 @@ runs:
|
||||
# * They are reused through the Turborepo cache
|
||||
- shell: bash
|
||||
name: Build packages
|
||||
if: ${{ inputs.BUILD == 'all' }}
|
||||
run: pnpm build:all
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
- shell: bash
|
||||
name: Build everything in the monorepo
|
||||
if: ${{ inputs.BUILD == 'default' }}
|
||||
run: pnpm build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
|
||||
108
.github/actions/nhost-cli/README.md
vendored
Normal file
108
.github/actions/nhost-cli/README.md
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
# Nhost CLI GitHub Action
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
```
|
||||
|
||||
### Install the CLI and start the app
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
```
|
||||
|
||||
### Set another working directory
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: examples/react-apollo
|
||||
start: true
|
||||
```
|
||||
|
||||
### Don't wait for the app to be ready
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI and start app
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
wait: false
|
||||
```
|
||||
|
||||
### Stop the app
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start app
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
- name: Do something
|
||||
cmd: echo "do something"
|
||||
- name: Stop
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
stop: true
|
||||
```
|
||||
|
||||
### Install a given value of the CLI
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
version: v0.8.10
|
||||
```
|
||||
|
||||
### Inject values into nhost/config.yaml
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
config: |
|
||||
services:
|
||||
auth:
|
||||
image: nhost/hasura-auth:0.16.1
|
||||
```
|
||||
78
.github/actions/nhost-cli/action.yaml
vendored
Normal file
78
.github/actions/nhost-cli/action.yaml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Nhost CLI
|
||||
description: 'Action to install the Nhost CLI and to run an application'
|
||||
inputs:
|
||||
start:
|
||||
description: "Start the application. If false, the application won't be started"
|
||||
default: 'false'
|
||||
wait:
|
||||
description: 'If starting the application, wait until it is ready'
|
||||
default: 'true'
|
||||
stop:
|
||||
description: 'Stop the application'
|
||||
default: 'false'
|
||||
path:
|
||||
description: 'Path to the application'
|
||||
default: '.'
|
||||
version:
|
||||
description: 'Version of the Nhost CLI'
|
||||
default: 'latest'
|
||||
config:
|
||||
description: 'Values to be injected into nhost/config.yaml'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Check if Nhost CLI is already installed
|
||||
id: check-nhost-cli
|
||||
shell: bash
|
||||
# TODO check if the version is the same
|
||||
run: |
|
||||
if [ -z "$(which nhost)" ]
|
||||
then
|
||||
echo "installed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "installed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Install Nhost CLI
|
||||
if: ${{ steps.check-nhost-cli.outputs.installed == 'false' }}
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 3
|
||||
max_attempts: 3
|
||||
command: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
|
||||
- name: Set custom configuration
|
||||
if: ${{ inputs.config }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: config="${{ inputs.config }}" yq -i '. *= env(config)' nhost/config.yaml
|
||||
- name: Start the application
|
||||
if: ${{ inputs.start == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: nhost dev --no-browser &
|
||||
- name: Wait for the app to be ready
|
||||
id: wait
|
||||
if: ${{ inputs.start == 'true' && inputs.wait == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
continue-on-error: true
|
||||
run: |
|
||||
curl -sSf --connect-timeout 3 \
|
||||
--max-time 5 \
|
||||
--retry 300 \
|
||||
--retry-delay 1 \
|
||||
--retry-max-time 300 \
|
||||
--retry-connrefused \
|
||||
'http://localhost:9695' > /dev/null
|
||||
- name: Log on failure
|
||||
if: steps.wait.outcome == 'failure'
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: |
|
||||
nhost logs
|
||||
exit 1
|
||||
- name: Stop the application
|
||||
if: ${{ inputs.stop == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: nhost down
|
||||
25
.github/labeler.yml
vendored
Normal file
25
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
dashboard:
|
||||
- dashboard/**/*
|
||||
|
||||
documentation:
|
||||
- any:
|
||||
- docs/**/*
|
||||
- '!docs/docs/reference/docgen/**/*'
|
||||
|
||||
examples:
|
||||
- examples/**/*
|
||||
|
||||
sdk:
|
||||
- packages/**/*
|
||||
|
||||
integrations:
|
||||
- integrations/**/*
|
||||
|
||||
react:
|
||||
- '{packages,examples,integrations}/*react*/**/*'
|
||||
|
||||
nextjs:
|
||||
- '{packages,examples}/*next*/**/*'
|
||||
|
||||
vue:
|
||||
- '{packages,examples,integrations}/*vue*/**/*'
|
||||
23
.github/renovate.json
vendored
Normal file
23
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"docker-compose": {
|
||||
"enabled": true
|
||||
},
|
||||
"ignoreDeps": [
|
||||
"pnpm",
|
||||
"node",
|
||||
"@types/node"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"enabledManagers": [
|
||||
"npm",
|
||||
"dockerfile",
|
||||
"docker-compose",
|
||||
"github-actions"
|
||||
]
|
||||
}
|
||||
150
.github/workflows/changesets.yaml
vendored
150
.github/workflows/changesets.yaml
vendored
@@ -5,26 +5,34 @@ on:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'dashboard/**'
|
||||
- 'examples/**'
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- '!.changeset/**'
|
||||
- 'LICENSE'
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
DASHBOARD_PACKAGE: '@nhost/dashboard'
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: Version
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
|
||||
dashboardVersion: ${{ steps.dashboard.outputs.dashboardVersion }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# * Install Node and dependencies
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Create PR or Publish release
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
@@ -36,3 +44,137 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Check Dashboard tag
|
||||
id: dashboard
|
||||
if: steps.changesets.outputs.hasChangesets == 'false'
|
||||
run: |
|
||||
DASHBOARD_VERSION=$(jq -r .version dashboard/package.json)
|
||||
GIT_TAG="${{ env.DASHBOARD_PACKAGE}}@$DASHBOARD_VERSION"
|
||||
if [ -z "$(git tag -l | grep $GIT_TAG)" ]; then
|
||||
echo "dashboardVersion=$DASHBOARD_VERSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
test:
|
||||
needs: version
|
||||
name: Dashboard
|
||||
if: needs.version.outputs.dashboardVersion != ''
|
||||
uses: ./.github/workflows/dashboard.yaml
|
||||
secrets: inherit
|
||||
|
||||
publish-docker:
|
||||
name: Publish to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
- version
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Add git tag
|
||||
run: |
|
||||
git tag "${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}"
|
||||
git push origin --tags
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
nhost/dashboard
|
||||
tags: |
|
||||
type=raw,value=latest,enable=true
|
||||
type=semver,pattern={{version}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=semver,pattern={{major}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=sha
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push to Docker Hub
|
||||
uses: docker/build-push-action@v3
|
||||
timeout-minutes: 60
|
||||
with:
|
||||
context: .
|
||||
file: ./dashboard/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
TURBO_TOKEN=${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM=${{ env.TURBO_TEAM }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
- name: Create GitHub Release
|
||||
uses: taiki-e/create-gh-release-action@v1
|
||||
with:
|
||||
changelog: dashboard/CHANGELOG.md
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: ${{ env.DASHBOARD_PACKAGE }}@
|
||||
ref: refs/tags/${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
- name: Remove tag on failure
|
||||
if: failure()
|
||||
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
- name: Trigger a Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
|
||||
bump-cli:
|
||||
name: Bump Dashboard version in the Nhost CLI
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- version
|
||||
- publish-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: nhost/cli
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
fetch-depth: 0
|
||||
- name: Bump version in source code
|
||||
run: |
|
||||
IMAGE=$(echo ${{ env.DASHBOARD_PACKAGE }} | sed 's/@\(.\+\)\/\(.\+\)/\1\\\/\2/g')
|
||||
VERSION="${{ needs.version.outputs.dashboardVersion }}"
|
||||
EXPRESSION='s/"'$IMAGE':[0-9]\+\.[0-9]\+\.[0-9]\+"/"'$IMAGE':'$VERSION'"/g'
|
||||
find ./ -type f -exec sed -i -e $EXPRESSION {} \;
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
commit-message: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
|
||||
branch: bump-dashboard-version
|
||||
delete-branch: true
|
||||
title: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
|
||||
body: |
|
||||
This PR bumps the Nhost Dashboard Docker image to version ${{ needs.version.outputs.dashboardVersion }}.
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
name: Tests
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'dashboard/**'
|
||||
- 'docs/**'
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
@@ -13,15 +11,16 @@ on:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths-ignore:
|
||||
- 'dashboard/**'
|
||||
- 'docs/**'
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build @nhost packages
|
||||
@@ -33,21 +32,37 @@ jobs:
|
||||
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
BUILD: 'all'
|
||||
- name: Check if the pnpm lockfile changed
|
||||
id: changed-lockfile
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: pnpm-lock.yaml
|
||||
# * Determine a pnpm filter argument for packages that have been modified.
|
||||
# * If the lockfile has changed, we don't filter anything in order to run all the e2e tests.
|
||||
- name: filter packages
|
||||
id: filter-packages
|
||||
if: steps.changed-lockfile.outputs.any_changed != 'true' && github.event_name == 'pull_request'
|
||||
run: echo "filter=${{ format('--filter=...[origin/{0}]', github.base_ref) }}" >> $GITHUB_OUTPUT
|
||||
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
|
||||
# * In a PR, only include packages that have been modified, and their dependencies
|
||||
- name: List examples with an e2e script
|
||||
id: set-matrix
|
||||
run: |
|
||||
FILTER_MODIFIED="${{ github.event_name == 'pull_request' && format('--filter=...[origin/{0}]', github.base_ref) || '' }}"
|
||||
PACKAGES=$(pnpm recursive list --depth -1 --parseable --filter='!nhost-root' $FILTER_MODIFIED \
|
||||
PACKAGES=$(pnpm recursive list --depth -1 --parseable --filter='!nhost-root' ${{ steps.filter-packages.outputs.filter }} \
|
||||
| xargs -I@ realpath --relative-to=$PWD @ \
|
||||
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
||||
| awk "!/null/" \
|
||||
| jq -c --slurp)
|
||||
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
e2e:
|
||||
name: 'e2e: ${{ matrix.package.name }}'
|
||||
name: 'e2e (${{ matrix.package.path }})'
|
||||
needs: build
|
||||
if: ${{ needs.build.outputs.matrix != '[]' && needs.build.outputs.matrix != '' }}
|
||||
strategy:
|
||||
@@ -61,10 +76,13 @@ jobs:
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Install Nhost CLI if a `nhost/config.yaml` file is found
|
||||
- name: Install Nhost CLI
|
||||
if: hashFiles(format('{0}/nhost/config.yaml', matrix.package.path)) != ''
|
||||
run: curl -L https://raw.githubusercontent.com/nhost/cli/main/get.sh | bash
|
||||
uses: ./.github/actions/nhost-cli
|
||||
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
|
||||
- name: Run e2e test
|
||||
run: pnpm --filter="${{ matrix.package.name }}" run e2e
|
||||
@@ -83,6 +101,7 @@ jobs:
|
||||
path: |
|
||||
${{format('{0}/cypress/screenshots/**', matrix.package.path)}}
|
||||
${{format('{0}/cypress/videos/**', matrix.package.path)}}
|
||||
|
||||
unit:
|
||||
name: Unit tests
|
||||
needs: build
|
||||
@@ -92,11 +111,14 @@ jobs:
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Run every `test` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Run unit tests
|
||||
run: pnpm run test
|
||||
run: pnpm run test:all
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: '**/coverage/coverage-final.json'
|
||||
name: codecov-umbrella
|
||||
@@ -104,6 +126,7 @@ jobs:
|
||||
run: |
|
||||
echo '### Code coverage' >> $GITHUB_STEP_SUMMARY
|
||||
echo 'Visit [codecov](https://app.codecov.io/gh/nhost/nhost/) to see the code coverage reports' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: build
|
||||
@@ -113,6 +136,9 @@ jobs:
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
run: pnpm run lint:all
|
||||
15
.github/workflows/contributors.yaml
vendored
15
.github/workflows/contributors.yaml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Add contributors
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
contrib-readme-job:
|
||||
runs-on: ubuntu-latest
|
||||
name: A job to automate contrib in readme
|
||||
steps:
|
||||
- name: Contribute List
|
||||
uses: akhilmhdh/contributors-readme-action@v2.3.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
46
.github/workflows/dashboard.yaml
vendored
46
.github/workflows/dashboard.yaml
vendored
@@ -1,73 +1,49 @@
|
||||
name: 'Dashboard'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'dashboard/**'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- 'dashboard/**'
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Build the application
|
||||
run: pnpm build:dashboard
|
||||
- uses: actions/cache@v3.0.11
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
- uses: actions/cache@v3.0.11
|
||||
id: restore-build
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Run tests
|
||||
run: pnpm test:dashboard
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
- uses: actions/cache@v3.0.11
|
||||
id: restore-build
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- run: pnpm lint:dashboard
|
||||
|
||||
15
.github/workflows/labeler.yaml
vendored
Normal file
15
.github/workflows/labeler.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: 'Pull Request Labeler'
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: '${{ secrets.GH_PAT }}'
|
||||
sync-labels: ''
|
||||
89
.github/workflows/renovate.yaml
vendored
Normal file
89
.github/workflows/renovate.yaml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: Renovate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [closed]
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
|
||||
jobs:
|
||||
renovate-changeset:
|
||||
name: Add changeset
|
||||
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'renovate/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
BUILD: 'none'
|
||||
- name: Determine bumps
|
||||
id: bumps
|
||||
run: |
|
||||
LAST_NON_PR_SHA=$(git log --no-merges main origin/${{ github.head_ref }} --format=format:%h -- | head -2 | tail -1)
|
||||
echo "result<<EOF" >> $GITHUB_OUTPUT
|
||||
pnpm recursive list --depth -1 --parseable \
|
||||
--filter='!nhost-root' \
|
||||
--filter=[$LAST_NON_PR_SHA] \
|
||||
| xargs -I@ jq ".name" @/package.json \
|
||||
| sort \
|
||||
| uniq -u \
|
||||
| awk '$0=$0": patch"' \
|
||||
>> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
- name: Install dictionary
|
||||
if: steps.bumps.outputs.result != ''
|
||||
run: sudo apt-get install wbritish
|
||||
- name: Generate changeset file name
|
||||
id: file_name
|
||||
if: steps.bumps.outputs.result != ''
|
||||
run: |
|
||||
FILE_NAME=$(shuf -n 3 /usr/share/dict/words | tr '\n' '-' | sed 's/-$//' | sed 's/'"'"'s//g' | tr '[:upper:]' '[:lower:]')
|
||||
echo "result=./.changeset/${FILE_NAME}.md" >> $GITHUB_OUTPUT
|
||||
- name: Create changeset file
|
||||
if: steps.bumps.outputs.result != ''
|
||||
run: |
|
||||
cat <<EOF > ${{ steps.file_name.outputs.result }}
|
||||
---
|
||||
${{ steps.bumps.outputs.result }}
|
||||
---
|
||||
|
||||
${{ github.event.pull_request.title }}
|
||||
EOF
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
commit-message: ${{ github.event.pull_request.title }}
|
||||
branch: renovate-changesets
|
||||
delete-branch: true
|
||||
title: 'chore: create changesest from Renovate bumps'
|
||||
labels: |
|
||||
dependencies
|
||||
body: |
|
||||
This PR creates the changesets from the Renovate dependencies that have been merged to main.
|
||||
- name: Enable Pull Request Automerge
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
- name: Auto approve
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
uses: juliangruber/approve-pull-request-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GH_PAT }}
|
||||
number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
104
.github/workflows/test-nhost-cli-action.yaml
vendored
Normal file
104
.github/workflows/test-nhost-cli-action.yaml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Test Nhost CLI action
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- '.github/actions/nhost-cli/**'
|
||||
- '!.github/actions/nhost-cli/**/*.md'
|
||||
|
||||
jobs:
|
||||
install:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
- name: should succeed running the nhost command
|
||||
run: nhost
|
||||
|
||||
start:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
- name: should be running
|
||||
run: curl -sSf 'http://localhost:9695' > /dev/null
|
||||
|
||||
stop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI, start and stop the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
stop: true
|
||||
- name: should have no live docker container
|
||||
run: |
|
||||
if [ -z "docker ps -q" ]; then
|
||||
echo "Some docker containers are still running"
|
||||
docker ps
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
wait: false
|
||||
- name: should not be ready
|
||||
run: curl -sSf -o /dev/null 'http://localhost:9695' > /dev/null && exit 1 || true
|
||||
- name: should eventually be ready
|
||||
run: |
|
||||
curl -sSf --connect-timeout 3 \
|
||||
--max-time 5 \
|
||||
--retry 300 \
|
||||
--retry-delay 1 \
|
||||
--retry-max-time 300 \
|
||||
--retry-connrefused \
|
||||
'http://localhost:9695' > /dev/null
|
||||
|
||||
config:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and run the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
config: |
|
||||
services:
|
||||
auth:
|
||||
image: nhost/hasura-auth:0.15.0
|
||||
- name: should find the injected hasura-auth version
|
||||
run: |
|
||||
VERSION=$(curl -sSf 'http://localhost:1337/v1/auth/version')
|
||||
EXPECTED_VERSION='{"version":"v0.15.0"}'
|
||||
if [ "$VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "Expected version $EXPECTED_VERSION but got $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
version: v0.8.10
|
||||
- name: should find the correct version
|
||||
run: nhost version | head -n 1 | grep v0.8.10 || exit 1
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -48,6 +48,10 @@ todo.md
|
||||
.netlify
|
||||
.monorepo-example
|
||||
|
||||
# Local Vercel folder
|
||||
.vercel
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# TypeDoc output
|
||||
|
||||
@@ -14,4 +14,5 @@ package.json
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
*.d.ts
|
||||
.next
|
||||
.next
|
||||
**/pnpm-lock.yaml
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": ["./dashboard"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
- This repository works with **Node 16**
|
||||
|
||||
- We use [pnpm](https://pnpm.io/) as a package manager to speed up development and builds, and as a basis for our monorepo. You need to make sure it's installed on your machine. There are [several ways to install it](https://pnpm.io/installation), but the easiest way is with `npm`:
|
||||
|
||||
```sh
|
||||
@@ -97,6 +99,7 @@ You can take a look at the changeset documentation: [How to add a changeset](htt
|
||||
|
||||
You'll notice that `git commit` takes a few seconds to run. We set a commit hook that scans the changes in the code, automatically generates documentation from the inline [TSDoc](https://tsdoc.org/) annotations, and adds these generated documentation files to the commit. They automatically update the [reference documentation](https://docs.nhost.io/reference).
|
||||
|
||||
|
||||
<!-- ## Good practices
|
||||
- lint
|
||||
- prettier
|
||||
|
||||
406
README.md
406
README.md
@@ -101,14 +101,17 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
- [JavaScript/TypeScript SDK](https://docs.nhost.io/reference/javascript)
|
||||
- [Dart and Flutter SDK](https://github.com/nhost/nhost-dart)
|
||||
- [Nhost React](https://docs.nhost.io/reference/react)
|
||||
- [Nhost Next.js](https://docs.nhost.io/reference/nextjs)
|
||||
- [Nhost Vue](https://docs.nhost.io/reference/vue)
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript)
|
||||
- [Dart and Flutter](https://github.com/nhost/nhost-dart)
|
||||
- [React](https://docs.nhost.io/reference/react)
|
||||
- [Next.js](https://docs.nhost.io/reference/nextjs)
|
||||
- [Vue](https://docs.nhost.io/reference/vue)
|
||||
|
||||
## Integrations
|
||||
|
||||
- [Apollo](./integrations/apollo#nhostapollo)
|
||||
- [React Apollo](./integrations/react-apollo#nhostreact-apollo)
|
||||
- [React URQL](./integrations/react-urql#nhostreact-urql)
|
||||
- [Stripe GraphQL API](./integrations/stripe-graphql-js#nhoststripe-graphql-js)
|
||||
- [Google Translation GraphQL API](./integrations/google-translation#nhostgoogle-translation)
|
||||
|
||||
@@ -127,8 +130,8 @@ Also, follow Nhost on [GitHub Discussions](https://github.com/nhost/nhost/discus
|
||||
|
||||
This repository, and most of our other open source projects, are licensed under the MIT license.
|
||||
|
||||
<a href="https://runacap.com/ross-index/q1-2022/" target="_blank" rel="noopener">
|
||||
<img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2022/06/ROSS_badge_black_Q1_2022.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q1 2022 | Runa Capital" width="260" height="56" />
|
||||
<a href="https://runacap.com/ross-index/" target="_blank" rel="noopener" >
|
||||
<img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2022/06/ROSS_black_edition_badge.svg" alt="ROSS Index - Fastest Growing Open-Source Startups | Runa Capital" width="260" height="56" />
|
||||
</a>
|
||||
|
||||
### How to contribute
|
||||
@@ -141,387 +144,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
|
||||
### Contributors
|
||||
|
||||
<!-- readme: contributors -start -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/plmercereau">
|
||||
<img src="https://avatars.githubusercontent.com/u/24897252?v=4" width="100;" alt="plmercereau"/>
|
||||
<br />
|
||||
<sub><b>Pilou</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/elitan">
|
||||
<img src="https://avatars.githubusercontent.com/u/331818?v=4" width="100;" alt="elitan"/>
|
||||
<br />
|
||||
<sub><b>Johan Eliasson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/szilarddoro">
|
||||
<img src="https://avatars.githubusercontent.com/u/310881?v=4" width="100;" alt="szilarddoro"/>
|
||||
<br />
|
||||
<sub><b>Szilárd Dóró</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nunopato">
|
||||
<img src="https://avatars.githubusercontent.com/u/1523504?v=4" width="100;" alt="nunopato"/>
|
||||
<br />
|
||||
<sub><b>Nuno Pato</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/gdangelo">
|
||||
<img src="https://avatars.githubusercontent.com/u/4352286?v=4" width="100;" alt="gdangelo"/>
|
||||
<br />
|
||||
<sub><b>Grégory D'Angelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/guicurcio">
|
||||
<img src="https://avatars.githubusercontent.com/u/20285232?v=4" width="100;" alt="guicurcio"/>
|
||||
<br />
|
||||
<sub><b>Guido Curcio</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subatuba21">
|
||||
<img src="https://avatars.githubusercontent.com/u/34824571?v=4" width="100;" alt="subatuba21"/>
|
||||
<br />
|
||||
<sub><b>Subha Das</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/sebagudelo">
|
||||
<img src="https://avatars.githubusercontent.com/u/43288271?v=4" width="100;" alt="sebagudelo"/>
|
||||
<br />
|
||||
<sub><b>Sebagudelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mrinalwahal">
|
||||
<img src="https://avatars.githubusercontent.com/u/9859731?v=4" width="100;" alt="mrinalwahal"/>
|
||||
<br />
|
||||
<sub><b>Mrinal Wahal</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/timpratim">
|
||||
<img src="https://avatars.githubusercontent.com/u/32492961?v=4" width="100;" alt="timpratim"/>
|
||||
<br />
|
||||
<sub><b>Pratim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/chrtze">
|
||||
<img src="https://avatars.githubusercontent.com/u/3797215?v=4" width="100;" alt="chrtze"/>
|
||||
<br />
|
||||
<sub><b>Christopher Möller</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/GavanWilhite">
|
||||
<img src="https://avatars.githubusercontent.com/u/2085119?v=4" width="100;" alt="GavanWilhite"/>
|
||||
<br />
|
||||
<sub><b>Gavan Wilhite</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/FuzzyReason">
|
||||
<img src="https://avatars.githubusercontent.com/u/62517920?v=4" width="100;" alt="FuzzyReason"/>
|
||||
<br />
|
||||
<sub><b>Vadim Smirnov</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ejkkan">
|
||||
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
|
||||
<br />
|
||||
<sub><b>Erik Magnusson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/macmac49">
|
||||
<img src="https://avatars.githubusercontent.com/u/831190?v=4" width="100;" alt="macmac49"/>
|
||||
<br />
|
||||
<sub><b>Macmac49</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subhendukundu">
|
||||
<img src="https://avatars.githubusercontent.com/u/20059141?v=4" width="100;" alt="subhendukundu"/>
|
||||
<br />
|
||||
<sub><b>Subhendu Kundu</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/heygambo">
|
||||
<img src="https://avatars.githubusercontent.com/u/449438?v=4" width="100;" alt="heygambo"/>
|
||||
<br />
|
||||
<sub><b>Christian Gambardella</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dbarrosop">
|
||||
<img src="https://avatars.githubusercontent.com/u/6246622?v=4" width="100;" alt="dbarrosop"/>
|
||||
<br />
|
||||
<sub><b>David Barroso</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/hajek-raven">
|
||||
<img src="https://avatars.githubusercontent.com/u/7288737?v=4" width="100;" alt="hajek-raven"/>
|
||||
<br />
|
||||
<sub><b>Filip Hájek</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MelodicCrypter">
|
||||
<img src="https://avatars.githubusercontent.com/u/18341500?v=4" width="100;" alt="MelodicCrypter"/>
|
||||
<br />
|
||||
<sub><b>Hugh Caluscusin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jerryjappinen">
|
||||
<img src="https://avatars.githubusercontent.com/u/1101002?v=4" width="100;" alt="jerryjappinen"/>
|
||||
<br />
|
||||
<sub><b>Jerry Jäppinen</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mdp18">
|
||||
<img src="https://avatars.githubusercontent.com/u/11698527?v=4" width="100;" alt="mdp18"/>
|
||||
<br />
|
||||
<sub><b>Max</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mustafa-hanif">
|
||||
<img src="https://avatars.githubusercontent.com/u/30019262?v=4" width="100;" alt="mustafa-hanif"/>
|
||||
<br />
|
||||
<sub><b>Mustafa Hanif</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nbourdin">
|
||||
<img src="https://avatars.githubusercontent.com/u/5602476?v=4" width="100;" alt="nbourdin"/>
|
||||
<br />
|
||||
<sub><b>Nicolas Bourdin</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/piromsurang">
|
||||
<img src="https://avatars.githubusercontent.com/u/17776837?v=4" width="100;" alt="piromsurang"/>
|
||||
<br />
|
||||
<sub><b>Piromsurang Rungserichai</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Savinvadim1312">
|
||||
<img src="https://avatars.githubusercontent.com/u/16936043?v=4" width="100;" alt="Savinvadim1312"/>
|
||||
<br />
|
||||
<sub><b>Savin Vadim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Svarto">
|
||||
<img src="https://avatars.githubusercontent.com/u/24279217?v=4" width="100;" alt="Svarto"/>
|
||||
<br />
|
||||
<sub><b>Svarto</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/muttenzer">
|
||||
<img src="https://avatars.githubusercontent.com/u/49474412?v=4" width="100;" alt="muttenzer"/>
|
||||
<br />
|
||||
<sub><b>Muttenzer</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ahmic">
|
||||
<img src="https://avatars.githubusercontent.com/u/13452362?v=4" width="100;" alt="ahmic"/>
|
||||
<br />
|
||||
<sub><b>Amir Ahmic</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/akd-io">
|
||||
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
||||
<br />
|
||||
<sub><b>Anders Kjær Damgaard</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Sonichigo">
|
||||
<img src="https://avatars.githubusercontent.com/u/53110238?v=4" width="100;" alt="Sonichigo"/>
|
||||
<br />
|
||||
<sub><b>Animesh Pathak</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rustyb">
|
||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||
<br />
|
||||
<sub><b>Colin Broderick</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/daguitosama">
|
||||
<img src="https://avatars.githubusercontent.com/u/34744883?v=4" width="100;" alt="daguitosama"/>
|
||||
<br />
|
||||
<sub><b>Dago</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dminkovsky">
|
||||
<img src="https://avatars.githubusercontent.com/u/218725?v=4" width="100;" alt="dminkovsky"/>
|
||||
<br />
|
||||
<sub><b>Dmitry Minkovsky</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dohomi">
|
||||
<img src="https://avatars.githubusercontent.com/u/489221?v=4" width="100;" alt="dohomi"/>
|
||||
<br />
|
||||
<sub><b>Dominic Garms</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/gaurav1999">
|
||||
<img src="https://avatars.githubusercontent.com/u/20752142?v=4" width="100;" alt="gaurav1999"/>
|
||||
<br />
|
||||
<sub><b>Gaurav Agrawal</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alveshelio">
|
||||
<img src="https://avatars.githubusercontent.com/u/8176422?v=4" width="100;" alt="alveshelio"/>
|
||||
<br />
|
||||
<sub><b>Helio Alves</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nkhdo">
|
||||
<img src="https://avatars.githubusercontent.com/u/26102306?v=4" width="100;" alt="nkhdo"/>
|
||||
<br />
|
||||
<sub><b>Hoang Do</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/eltociear">
|
||||
<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/>
|
||||
<br />
|
||||
<sub><b>Ikko Ashimine</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jladuval">
|
||||
<img src="https://avatars.githubusercontent.com/u/1935359?v=4" width="100;" alt="jladuval"/>
|
||||
<br />
|
||||
<sub><b>Jacob Duval</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/leothorp">
|
||||
<img src="https://avatars.githubusercontent.com/u/12928449?v=4" width="100;" alt="leothorp"/>
|
||||
<br />
|
||||
<sub><b>Leo Thorp</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/LucasBois1">
|
||||
<img src="https://avatars.githubusercontent.com/u/44686060?v=4" width="100;" alt="LucasBois1"/>
|
||||
<br />
|
||||
<sub><b>Lucas Bois</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MarcelloTheArcane">
|
||||
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
|
||||
<br />
|
||||
<sub><b>Max Reynolds</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nachoaldamav">
|
||||
<img src="https://avatars.githubusercontent.com/u/22749943?v=4" width="100;" alt="nachoaldamav"/>
|
||||
<br />
|
||||
<sub><b>Nacho Aldama</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ghoshnirmalya">
|
||||
<img src="https://avatars.githubusercontent.com/u/6391763?v=4" width="100;" alt="ghoshnirmalya"/>
|
||||
<br />
|
||||
<sub><b>Nirmalya Ghosh</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/quentin-decre">
|
||||
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
|
||||
<br />
|
||||
<sub><b>Quentin Decré</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/altschuler">
|
||||
<img src="https://avatars.githubusercontent.com/u/956928?v=4" width="100;" alt="altschuler"/>
|
||||
<br />
|
||||
<sub><b>Simon Altschuler</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/atapas">
|
||||
<img src="https://avatars.githubusercontent.com/u/3633137?v=4" width="100;" alt="atapas"/>
|
||||
<br />
|
||||
<sub><b>Tapas Adhikary</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/uulwake">
|
||||
<img src="https://avatars.githubusercontent.com/u/22399181?v=4" width="100;" alt="uulwake"/>
|
||||
<br />
|
||||
<sub><b>Ulrich Wake</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/kwarabei">
|
||||
<img src="https://avatars.githubusercontent.com/u/102731455?v=4" width="100;" alt="kwarabei"/>
|
||||
<br />
|
||||
<sub><b>Vadim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/TheRedLancer">
|
||||
<img src="https://avatars.githubusercontent.com/u/58493767?v=4" width="100;" alt="TheRedLancer"/>
|
||||
<br />
|
||||
<sub><b>Zach Burnaby</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/komninoschat">
|
||||
<img src="https://avatars.githubusercontent.com/u/29049104?v=4" width="100;" alt="komninoschat"/>
|
||||
<br />
|
||||
<sub><b>Komninos</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/meesvandongen">
|
||||
<img src="https://avatars.githubusercontent.com/u/35409045?v=4" width="100;" alt="meesvandongen"/>
|
||||
<br />
|
||||
<sub><b>Meesvandongen</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<!-- readme: contributors -end -->
|
||||
<a href="https://github.com/nhost/nhost/graphs/contributors">
|
||||
<p align="center">
|
||||
<img width="720" src="https://contrib.rocks/image?repo=nhost/nhost" alt="A table of avatars from the project's contributors" />
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -19,9 +19,11 @@ module.exports = {
|
||||
'*.spec.ts',
|
||||
'*.spec.tsx',
|
||||
'tests/**/*.ts',
|
||||
'tests/**/*.d.ts'
|
||||
'tests/**/*.d.ts',
|
||||
'e2e/**/*.ts',
|
||||
'e2e/**/*.d.ts'
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'simple-import-sort', 'cypress'],
|
||||
plugins: ['@typescript-eslint', 'cypress'],
|
||||
extends: ['plugin:cypress/recommended'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
@@ -30,31 +32,6 @@ module.exports = {
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
// Node.js builtins. You could also generate this regex if you use a `.js` config.
|
||||
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
|
||||
[
|
||||
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
|
||||
],
|
||||
// Packages
|
||||
['^\\w'],
|
||||
// Internal packages.
|
||||
['^(@|config/)(/*|$)'],
|
||||
// Side effect imports.
|
||||
['^\\u0000'],
|
||||
// Parent imports. Put `..` last.
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||
// Other relative imports. Put same-folder imports and `.` last.
|
||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
// Style imports.
|
||||
['^.+\\.s?css$']
|
||||
]
|
||||
}
|
||||
],
|
||||
'import/no-anonymous-default-export': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
module.exports = {
|
||||
'(packages|integrations)/(docgen|hasura-auth-js|hasura-storage-js|nextjs|nhost-js|react|core|vue)/src/**/*.{js,ts,jsx,tsx}':
|
||||
['pnpm docgen', 'git add docs'],
|
||||
'(nhost-cloud.yaml|**/nhost/config.yaml)': () => [
|
||||
'pnpm sync-versions',
|
||||
"git add ':(glob)**/nhost/config.yaml'"
|
||||
|
||||
@@ -28,22 +28,44 @@
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types", "**/*/dist", "**/*/build", "**/*/.next", "**/*/umd"
|
||||
"./node_modules/@types",
|
||||
"**/*/dist",
|
||||
"**/*/build",
|
||||
"**/*/.next",
|
||||
"**/*/umd"
|
||||
],
|
||||
"paths": {
|
||||
"@nhost/apollo": ["../packages/apollo/src/index.ts"],
|
||||
"@nhost/core": ["../packages/core/src/index.ts"],
|
||||
"@nhost/docgen": ["../packages/docgen/src/index.ts"],
|
||||
"@nhost/hasura-auth-js": ["../packages/hasura-auth-js/src/index.ts"],
|
||||
"@nhost/hasura-storage-js": ["../packages/hasura-storage-js/src/index.ts"],
|
||||
"@nhost/nextjs": ["../packages/nextjs/src/index.ts"],
|
||||
"@nhost/nhost-js": ["../packages/nhost-js/src/index.ts"],
|
||||
"@nhost/react": ["../packages/react/src/index.ts"],
|
||||
"@nhost/react-apollo": ["../packages/react-apollo/src/index.ts"],
|
||||
"@nhost/react-auth": ["../packages/react-auth/src/index.ts"],
|
||||
"@nhost/vue": ["../packages/vue/src/index.ts"]
|
||||
"@nhost/apollo": [
|
||||
"../integrations/apollo/src/index.ts"
|
||||
],
|
||||
"@nhost/docgen": [
|
||||
"../packages/docgen/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-auth-js": [
|
||||
"../packages/hasura-auth-js/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-storage-js": [
|
||||
"../packages/hasura-storage-js/src/index.ts"
|
||||
],
|
||||
"@nhost/nextjs": [
|
||||
"../packages/nextjs/src/index.ts"
|
||||
],
|
||||
"@nhost/nhost-js": [
|
||||
"../packages/nhost-js/src/index.ts"
|
||||
],
|
||||
"@nhost/react": [
|
||||
"../packages/react/src/index.ts"
|
||||
],
|
||||
"@nhost/react-apollo": [
|
||||
"../integrations/react-apollo/src/index.ts"
|
||||
],
|
||||
"@nhost/vue": [
|
||||
"../packages/vue/src/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
@@ -18,7 +19,9 @@ export default defineConfig({
|
||||
tsconfigPaths(),
|
||||
dts({
|
||||
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
|
||||
entryRoot: 'src'
|
||||
entryRoot: 'src',
|
||||
// Was defaulting to true until version 1.7
|
||||
skipDiagnostics: true
|
||||
})
|
||||
],
|
||||
test: {
|
||||
@@ -41,6 +44,12 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
external: (id) => deps.some((dep) => id.startsWith(dep)),
|
||||
plugins: [
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
'exports.hasOwnProperty(': 'Object.prototype.hasOwnProperty.call(exports,'
|
||||
})
|
||||
],
|
||||
output: {
|
||||
globals: {
|
||||
graphql: 'graphql',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['next', 'airbnb', 'airbnb-typescript', 'airbnb/hooks', 'prettier'],
|
||||
|
||||
@@ -6,6 +6,19 @@ module.exports = {
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-addon-next-router',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||
*/
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
@@ -26,4 +39,10 @@ module.exports = {
|
||||
},
|
||||
};
|
||||
},
|
||||
env: (config) => ({
|
||||
...config,
|
||||
NEXT_PUBLIC_ENV: 'dev',
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: 'http://localhost:1337',
|
||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -2,9 +2,25 @@ import '@fontsource/inter';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Buffer } from 'buffer';
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context';
|
||||
import '../src/styles/globals.css';
|
||||
import defaultTheme from '../src/theme/default';
|
||||
|
||||
global.Buffer = Buffer;
|
||||
|
||||
initialize({ onUnhandledRequest: 'bypass' });
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const parameters = {
|
||||
nextRouter: {
|
||||
Provider: RouterContext.Provider,
|
||||
isReady: true,
|
||||
},
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
@@ -14,11 +30,25 @@ export const parameters = {
|
||||
},
|
||||
};
|
||||
|
||||
export const withMuiTheme = (Story) => (
|
||||
<ThemeProvider theme={defaultTheme}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export const decorators = [withMuiTheme];
|
||||
export const decorators = [
|
||||
(Story) => (
|
||||
<ThemeProvider theme={defaultTheme}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostApolloProvider
|
||||
fetchPolicy="cache-first"
|
||||
graphqlUrl="http://localhost:1337/v1/graphql"
|
||||
>
|
||||
<Story />
|
||||
</NhostApolloProvider>
|
||||
),
|
||||
mswDecorator,
|
||||
];
|
||||
|
||||
204
dashboard/CHANGELOG.md
Normal file
204
dashboard/CHANGELOG.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9a1aa7bb: add functions to the log dashboard
|
||||
- f29abe62: feat(dashboard): Users Management v2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7766624b: feat(dashboard): add JWT secret editor modal
|
||||
- @nhost/react-apollo@4.12.1
|
||||
- @nhost/nextjs@1.12.1
|
||||
|
||||
## 0.7.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dd0738d5: fix(dashboard): provisioning status polling
|
||||
|
||||
## 0.7.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b21222b3: chore(deps): update dependency @types/node to v16
|
||||
- 9e0486a3: fix(dashboard): close modals when navigating
|
||||
- Updated dependencies [b21222b3]
|
||||
- Updated dependencies [65687bee]
|
||||
- Updated dependencies [54df0df4]
|
||||
- @nhost/nextjs@1.12.0
|
||||
- @nhost/react-apollo@4.12.0
|
||||
|
||||
## 0.7.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d6527122: fix(dashboard): use correct service URLs
|
||||
|
||||
## 0.7.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [57db5b83]
|
||||
- @nhost/nextjs@1.11.0
|
||||
- @nhost/nhost-js@1.7.0
|
||||
- @nhost/react@0.17.0
|
||||
- @nhost/react-apollo@4.11.0
|
||||
|
||||
## 0.7.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a6d31dc2: fix(dashboard): don't break the UI when project is not loaded yet
|
||||
|
||||
## 0.7.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7f251111: Use `NhostProvider` instead of `NhostReactProvider` and `NhostNextProvider`
|
||||
|
||||
`NhostReactProvider` and `NhostNextProvider` are now deprecated
|
||||
|
||||
- f4d70f88: fix(dashboard): do not break when region is nullish
|
||||
- 4a9471cc: Windows Live Provider displayed link updated to match backend url
|
||||
- 594488e4: fix(dashboard): do not show error when submitting Apple provider settings
|
||||
- Updated dependencies [7f251111]
|
||||
- @nhost/nextjs@1.10.0
|
||||
- @nhost/react@0.16.0
|
||||
- @nhost/react-apollo@4.10.0
|
||||
|
||||
## 0.7.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 80b604ad: fix(dashboard): use correct Hasura slug
|
||||
|
||||
## 0.7.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2d2beb53: fix(dashboard): prevent error on GraphQL page
|
||||
- ac8efcbd: chore(dashboard): deprecate old DNS name
|
||||
|
||||
## 0.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 132a4f4b: chore(dashboard): remove unused dependencies
|
||||
- 132a4f4b: chore(deps): synchronize @types/react-dom and @types/react versions
|
||||
- db57572f: fix(dashboard): correct section paddings when no env vars
|
||||
- Updated dependencies [132a4f4b]
|
||||
- @nhost/react@0.15.2
|
||||
- @nhost/react-apollo@4.9.2
|
||||
- @nhost/nextjs@1.9.3
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 34d85e54: chore(deps): update dependency critters to ^0.0.16
|
||||
- 9b93cf95: chore(deps): update dependency @netlify/functions to ^0.11.0
|
||||
- e0439030: chore(deps): update dependency @types/react-dom to v18.0.9
|
||||
- Updated dependencies [82124329]
|
||||
- @nhost/nextjs@1.9.2
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a1193da4: fix(dashboard): remove character limit from env var inputs
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 44f13f62: chore(dashboard): cleanup unused files
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e01cb2ed: chore(dashboard): change settings sidebar menu item density
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- db342f45: chore(dashboard): refactor Roles and Permissions settings sections
|
||||
- 8b9fa0b1: feat(dashboard): add Environment Variables page
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [66b4f3d0]
|
||||
- Updated dependencies [2e6923dc]
|
||||
- Updated dependencies [ef117c28]
|
||||
- Updated dependencies [aebb8225]
|
||||
- @nhost/core@0.9.4
|
||||
- @nhost/nhost-js@1.6.2
|
||||
- @nhost/nextjs@1.9.1
|
||||
- @nhost/react@0.15.1
|
||||
- @nhost/react-apollo@4.9.1
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- eef9c914: feat(dashboard): add Roles and Permissions page
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a48dd5bf: feat(dashboard): make backend port configurable
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5de965d9: fix(dashboard): alphabetic ordering of providers
|
||||
- b9087a4a: fix(dashboard): console -> dashboard terminology
|
||||
- ca012d79: docs(workos): WorkOS Docs
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 89bd37bc: fix(dashboard): correct redirect URL input opacity
|
||||
- Updated dependencies [4601d84e]
|
||||
- Updated dependencies [843087cb]
|
||||
- @nhost/react@0.15.0
|
||||
- @nhost/nextjs@1.9.0
|
||||
- @nhost/react-apollo@4.9.0
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 766cb612: fix(dashboard): correct redirect URL for oauth providers
|
||||
- Updated dependencies [53bdc294]
|
||||
- Updated dependencies [f2aaff05]
|
||||
- @nhost/nextjs@1.8.3
|
||||
- @nhost/core@0.9.3
|
||||
- @nhost/react@0.14.3
|
||||
- @nhost/nhost-js@1.6.1
|
||||
- @nhost/react-apollo@4.8.3
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9211743d: feat(dashboard): migrate Settings page features
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 73da6a67: fix(dashboard): avoid using BACKEND_URL locally
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- db118f97: feat(dashboard): generate Docker image
|
||||
55
dashboard/Dockerfile
Normal file
55
dashboard/Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
||||
FROM node:16-alpine AS pruner
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||
|
||||
FROM node:16-alpine AS builder
|
||||
ARG TURBO_TOKEN
|
||||
ARG TURBO_TEAM
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV NEXT_PUBLIC_ENV dev
|
||||
ENV NEXT_PUBLIC_NHOST_PLATFORM false
|
||||
|
||||
# placeholders for ports, will be replaced on runtime by entrypoint script
|
||||
ENV NEXT_PUBLIC_NHOST_MIGRATIONS_PORT __NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_PORT __NEXT_PUBLIC_NHOST_HASURA_PORT__
|
||||
ENV NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT __NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__
|
||||
|
||||
RUN yarn global add pnpm@7.17.0
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-*.yaml .
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY config/ config/
|
||||
RUN pnpm build:dashboard
|
||||
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
COPY --chown=nextjs:nodejs dashboard/docker-entrypoint.sh .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/next.config.js .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/package.json .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/public ./dashboard/public
|
||||
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/standalone/app ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/static ./dashboard/.next/static
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
CMD ["node", "dashboard/server.js"]
|
||||
@@ -30,31 +30,35 @@ First, you need to run the following command to start your backend locally:
|
||||
cd <your_nhost_project> && nhost dev
|
||||
```
|
||||
|
||||
Two environment variables are required to connect the Nhost Dashboard to your local backend:
|
||||
You can connect the Nhost Dashboard to your locally running backend by setting the following environment variables in `.env.development.local`:
|
||||
|
||||
- `NEXT_PUBLIC_NHOST_PLATFORM` should be set to `false`, because otherwise the Nhost Dashboard will try to connect to the Nhost platform.
|
||||
- `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` should be set to `http://localhost:9693` unless Hasura is configured to run on a different port. This is the URL of Hasura's migrations endpoint.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
```bash
|
||||
NEXT_PUBLIC_ENV=dev
|
||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
||||
```
|
||||
|
||||
### Storybook
|
||||
|
||||
Components are documented using [Storybook](https://storybook.js.org/). To run Storybook, run the following command:
|
||||
|
||||
```bash
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
### Full list of environment variables
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. |
|
||||
| `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` | URL of Hasura's migrations endpoint. Used only if local development is enabled. |
|
||||
| `NEXT_PUBLIC_NHOST_HASURA_URL` | URL of the Hasura Console. Used only when `NEXT_PUBLIC_ENV` is `dev`. |
|
||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | URL of the local backend. This is `http://localhost:1337` by default. |
|
||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
|
||||
| Name | Description |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. Setting this to `true` turns off local development. |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_MIGRATIONS_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `9693` |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_HASURA_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled and `NEXT_PUBLIC_ENV` is `dev`. Default: `9695` |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `1337` |
|
||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
|
||||
|
||||
## ESLint Rules
|
||||
|
||||
@@ -67,6 +71,7 @@ NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
||||
| `import/extensions` | JS / TS files should be imported without file extensions. |
|
||||
| `react/jsx-filename-extension` | JSX should only appear in `.jsx` and `.tsx` files. |
|
||||
| `react/jsx-no-bind` | Further investigation must be made on the performance impact of functions directly passed as props to components. |
|
||||
| `import/order` | Until we have a better auto-formatter, we disable this rule. |
|
||||
| `import/no-extraneous-dependencies` | `devDependencies` should be excluded from the list of disallowed imports. |
|
||||
| `curly` | By default it only enforces curly braces for multi-line blocks, but it should be enforced for single-line blocks as well. |
|
||||
| `no-restricted-exports` | `export { default } from './module'` is used heavily in `@/ui/v2` which is a restricted export by default. |
|
||||
|
||||
15
dashboard/docker-entrypoint.sh
Executable file
15
dashboard/docker-entrypoint.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# read ports from env variables or use defaults
|
||||
NEXT_PUBLIC_NHOST_MIGRATIONS_PORT="${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT:=9693}"
|
||||
NEXT_PUBLIC_NHOST_HASURA_PORT="${NEXT_PUBLIC_NHOST_HASURA_PORT:=9695}"
|
||||
NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT="${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT:=1337}"
|
||||
|
||||
# replace placeholders
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__/${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT}/g" {} +
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_HASURA_PORT__/${NEXT_PUBLIC_NHOST_HASURA_PORT}/g" {} +
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__/${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT}/g" {} +
|
||||
|
||||
exec "$@"
|
||||
@@ -13,11 +13,3 @@ generates:
|
||||
- 'typescript-react-apollo'
|
||||
config:
|
||||
withRefetchFn: true
|
||||
functions/utils/__generated__/graphql-request.ts:
|
||||
documents:
|
||||
- 'functions/**/*.graphql'
|
||||
- 'functions/**/*.gql'
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
- 'typescript-graphql-request'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const path = require('path');
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
@@ -5,6 +6,10 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
swcMinify: false,
|
||||
output: 'standalone',
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
},
|
||||
eslint: {
|
||||
dirs: ['src'],
|
||||
},
|
||||
@@ -61,6 +66,11 @@ module.exports = withBundleAnalyzer({
|
||||
destination: '/:workspaceSlug/:appSlug/settings/environment-variables',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/:workspaceSlug/:appSlug/users/:userId',
|
||||
destination: '/:workspaceSlug/:appSlug/users?userId=:userId',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.1.0",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -8,16 +8,16 @@
|
||||
"build": "next build --no-lint",
|
||||
"analyze": "ANALYZE=true pnpm build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings 6",
|
||||
"lint": "next lint --max-warnings 3",
|
||||
"test": "vitest",
|
||||
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
|
||||
"nhost:dev": "nhost dev -d",
|
||||
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook": "start-storybook -p 6006 -s public",
|
||||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.2",
|
||||
"@apollo/client": "^3.7.3",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@emotion/cache": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
@@ -25,25 +25,24 @@
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@graphiql/react": "^0.13.2",
|
||||
"@graphiql/react": "^0.15.0",
|
||||
"@graphiql/toolkit": "^0.8.0",
|
||||
"@headlessui/react": "^1.6.5",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@mui/base": "^5.0.0-alpha.105",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/system": "^5.10.13",
|
||||
"@mui/base": "^5.0.0-alpha.106",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/system": "^5.10.14",
|
||||
"@mui/x-date-pickers": "^5.0.8",
|
||||
"@nhost/core": "^0.9.1",
|
||||
"@nhost/nextjs": "^1.8.1",
|
||||
"@nhost/nhost-js": "^1.5.2",
|
||||
"@nhost/react": "^0.14.1",
|
||||
"@nhost/react-apollo": "^4.8.1",
|
||||
"@nhost/nextjs": "workspace:*",
|
||||
"@nhost/react-apollo": "workspace:*",
|
||||
"@segment/snippet": "^4.15.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-table": "^8.5.27",
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.22",
|
||||
"@tanstack/react-table": "^8.5.30",
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
||||
"analytics-node": "^6.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -51,30 +50,29 @@
|
||||
"cross-fetch": "^3.1.5",
|
||||
"date-fns": "^2.29.3",
|
||||
"generate-password": "^1.7.0",
|
||||
"graphiql": "^2.0.8",
|
||||
"graphiql": "^2.2.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^4.3.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "^12.3.1",
|
||||
"next-seo": "^5.14.1",
|
||||
"node-pg-format": "^1.3.5",
|
||||
"pluralize": "^8.0.0",
|
||||
"prettysize": "^2.0.0",
|
||||
"randomstring": "^1.2.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.39.3",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-is": "17.0.2",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"react-table": "^7.8.0",
|
||||
"sharp": "^0.31.2",
|
||||
"slugify": "^1.6.5",
|
||||
"smartlook-client": "^6.0.0",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^1.8.0",
|
||||
"utility-types": "^3.10.0",
|
||||
@@ -89,71 +87,63 @@
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.1",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||
"@netlify/functions": "^0.7.2",
|
||||
"@next/bundle-analyzer": "^12.3.1",
|
||||
"@storybook/addon-actions": "^6.5.13",
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
"@storybook/addon-interactions": "^6.5.13",
|
||||
"@storybook/addon-links": "^6.5.13",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.14",
|
||||
"@storybook/addon-interactions": "^6.5.14",
|
||||
"@storybook/addon-links": "^6.5.14",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.14",
|
||||
"@storybook/manager-webpack5": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.0.25",
|
||||
"@types/react-dom": "18.0.8",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/validator": "^13.7.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.0",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"@vitest/coverage-c8": "^0.25.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"@vitest/coverage-c8": "^0.26.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"concurrently": "^6.3.0",
|
||||
"critters": "^0.0.10",
|
||||
"csstype": "^3.0.10",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "^13.0.2",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"express": "^4.18.2",
|
||||
"express-validator": "^6.14.2",
|
||||
"jsdom": "^20.0.2",
|
||||
"jsdom": "^20.0.3",
|
||||
"lint-staged": ">=13",
|
||||
"msw": "^0.48.2",
|
||||
"msw": "^0.49.0",
|
||||
"msw-storybook-addon": "^1.6.3",
|
||||
"postcss": "^8.4.19",
|
||||
"postmark": "^2.7.8",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwind-css": "^1.5.0",
|
||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"storybook-addon-next-router": "^4.0.1",
|
||||
"tailwindcss": "^3.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.3",
|
||||
"vite-tsconfig-paths": "^3.5.2",
|
||||
"vitest": "^0.25.1",
|
||||
"vite": "^4.0.2",
|
||||
"vite-tsconfig-paths": "^4.0.3",
|
||||
"vitest": "^0.26.2",
|
||||
"webpack": "^5.75.0"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -167,5 +157,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
44
dashboard/public/assets/BR.svg
Normal file
44
dashboard/public/assets/BR.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 19 KiB |
1
dashboard/public/assets/twilio.svg
Normal file
1
dashboard/public/assets/twilio.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="21" height="21" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.08 0c5.578 0 10.08 4.507 10.08 10.09 0 5.584-4.502 10.09-10.08 10.09A10.072 10.072 0 0 1 0 10.09C0 4.507 4.503 0 10.08 0Zm0 2.69a7.375 7.375 0 0 0-7.392 7.4c0 4.104 3.293 7.4 7.392 7.4 4.1 0 7.392-3.296 7.392-7.4 0-4.103-3.293-7.4-7.392-7.4Zm-2.486 7.804c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086a2.095 2.095 0 0 1-2.083-2.086c0-1.143.94-2.085 2.083-2.085Zm4.973 0c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086a2.095 2.095 0 0 1-2.084-2.086c0-1.143.941-2.085 2.084-2.085Zm0-4.978c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086A2.095 2.095 0 0 1 10.483 7.6c0-1.143.941-2.085 2.084-2.085Zm-4.973 0c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086A2.095 2.095 0 0 1 5.51 7.6c0-1.143.94-2.085 2.083-2.085Z" fill="#F22F46"/></svg>
|
||||
|
After Width: | Height: | Size: 869 B |
303
dashboard/public/mockServiceWorker.js
Normal file
303
dashboard/public/mockServiceWorker.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker (0.49.0).
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
|
||||
const activeClientIds = new Set()
|
||||
|
||||
self.addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener('message', async function (event) {
|
||||
const clientId = event.source.id
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: INTEGRITY_CHECKSUM,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_DEACTIVATE': {
|
||||
activeClientIds.delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const { request } = event
|
||||
const accept = request.headers.get('accept') || ''
|
||||
|
||||
// Bypass server-sent events.
|
||||
if (accept.includes('text/event-stream')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been deleted (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID.
|
||||
const requestId = Math.random().toString(16).slice(2)
|
||||
|
||||
event.respondWith(
|
||||
handleRequest(event, requestId).catch((error) => {
|
||||
if (error.name === 'NetworkError') {
|
||||
console.warn(
|
||||
'[MSW] Successfully emulated a network error for the "%s %s" request.',
|
||||
request.method,
|
||||
request.url,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// At this point, any exception indicates an issue with the original request/response.
|
||||
console.error(
|
||||
`\
|
||||
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
|
||||
request.method,
|
||||
request.url,
|
||||
`${error.name}: ${error.message}`,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
;(async function () {
|
||||
const clonedResponse = response.clone()
|
||||
sendToClient(client, {
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
type: clonedResponse.type,
|
||||
ok: clonedResponse.ok,
|
||||
status: clonedResponse.status,
|
||||
statusText: clonedResponse.statusText,
|
||||
body:
|
||||
clonedResponse.body === null ? null : await clonedResponse.text(),
|
||||
headers: Object.fromEntries(clonedResponse.headers.entries()),
|
||||
redirected: clonedResponse.redirected,
|
||||
},
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Resolve the main client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
const clonedRequest = request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const headers = Object.fromEntries(clonedRequest.headers.entries())
|
||||
|
||||
// Remove MSW-specific request headers so the bypassed requests
|
||||
// comply with the server's CORS preflight check.
|
||||
// Operate with the headers as an object because request "Headers"
|
||||
// are immutable.
|
||||
delete headers['x-msw-bypass']
|
||||
|
||||
return fetch(clonedRequest, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass requests with the explicit bypass header.
|
||||
// Such requests can be issued by "ctx.fetch()".
|
||||
if (request.headers.get('x-msw-bypass') === 'true') {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const clientMessage = await sendToClient(client, {
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
mode: request.mode,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.text(),
|
||||
bodyUsed: request.bodyUsed,
|
||||
keepalive: request.keepalive,
|
||||
},
|
||||
})
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'MOCK_NOT_FOUND': {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
case 'NETWORK_ERROR': {
|
||||
const { name, message } = clientMessage.data
|
||||
const networkError = new Error(message)
|
||||
networkError.name = name
|
||||
|
||||
// Rejecting a "respondWith" promise emulates a network error.
|
||||
throw networkError
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
function sendToClient(client, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [channel.port2])
|
||||
})
|
||||
}
|
||||
|
||||
function sleep(timeMs) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, timeMs)
|
||||
})
|
||||
}
|
||||
|
||||
async function respondWithMock(response) {
|
||||
await sleep(response.delay)
|
||||
return new Response(response.body, response)
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import features from '@/data/features.json';
|
||||
import {
|
||||
useGetWorkspaceMembersQuery,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { Modal } from '@/ui';
|
||||
import Status, { StatusEnum } from '@/ui/Status';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import ChevronDownIcon from '@/ui/v2/icons/ChevronDownIcon';
|
||||
import Tooltip from '@/ui/v2/Tooltip';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { getCurrentEnvironment, isDevOrStaging } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { ChangeApplicationName } from './ChangeApplicationName';
|
||||
import ResetDatabasePasswordForm from './overview/ResetDatabasePasswordForm';
|
||||
import { RemoveApplicationModal } from './RemoveApplicationModal';
|
||||
|
||||
const isK8SPostgresEnabledInCurrentEnvironment = features[
|
||||
'k8s-postgres'
|
||||
].enabled.find((e) => e === getCurrentEnvironment());
|
||||
|
||||
export function ApplicationMenuItems() {
|
||||
const { currentApplication, currentWorkspace } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const [updateApplication, { client }] = useUpdateApplicationMutation();
|
||||
const { openAlertDialog } = useDialog();
|
||||
const user = useUserData();
|
||||
const [changeApplicationNameModal, setChangeApplicationNameModal] =
|
||||
useState(false);
|
||||
const [deleteApplicationModal, setDeleteApplicationModal] = useState(false);
|
||||
|
||||
const isProjectUsingRDS = currentApplication?.featureFlags?.find(
|
||||
(feature) => feature.name === 'fleetcontrol_use_rds',
|
||||
);
|
||||
|
||||
async function handleTriggerPausing() {
|
||||
try {
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
desiredState: ApplicationStatus.Paused,
|
||||
},
|
||||
},
|
||||
});
|
||||
await updateOwnCache(client);
|
||||
discordAnnounce(`${currentApplication.name} set to pause.`);
|
||||
triggerToast(`${currentApplication.name} set to pause.`);
|
||||
} catch (e) {
|
||||
triggerToast(`Error trying to pause ${currentApplication.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { data: workspaceData, loading } = useGetWorkspaceMembersQuery({
|
||||
variables: { workspaceId: currentWorkspace.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOwner = workspaceData.workspace.workspaceMembers.some(
|
||||
(member) => member.user.id === user.id && member.type === 'owner',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={changeApplicationNameModal}
|
||||
close={() => setChangeApplicationNameModal(!changeApplicationNameModal)}
|
||||
Component={ChangeApplicationName}
|
||||
/>
|
||||
<Modal
|
||||
showModal={deleteApplicationModal}
|
||||
close={() => setDeleteApplicationModal(!deleteApplicationModal)}
|
||||
Component={RemoveApplicationModal}
|
||||
/>
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger asChild hideChevron>
|
||||
<Button
|
||||
endIcon={<ChevronDownIcon className="h-4 w-4" />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
Project Settings
|
||||
</Button>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
className="mt-1"
|
||||
>
|
||||
<Dropdown.Item
|
||||
className="font-display text-sm font-medium text-dark"
|
||||
onClick={() => setChangeApplicationNameModal(true)}
|
||||
>
|
||||
Change Project Name
|
||||
</Dropdown.Item>
|
||||
{isDevOrStaging() && (
|
||||
<Dropdown.Item
|
||||
className="font-display text-sm font-medium text-dark"
|
||||
onClick={handleTriggerPausing}
|
||||
>
|
||||
<Status status={StatusEnum.Deploying}>Internal</Status>
|
||||
<span className="ml-2 align-middle">Pause App</span>
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{isK8SPostgresEnabledInCurrentEnvironment && !isProjectUsingRDS && (
|
||||
<Dropdown.Item
|
||||
className="font-display text-sm font-medium text-dark"
|
||||
onClick={() => {
|
||||
openAlertDialog({
|
||||
title: 'Reset Database Password',
|
||||
payload: <ResetDatabasePasswordForm />,
|
||||
props: {
|
||||
hidePrimaryAction: true,
|
||||
hideSecondaryAction: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reset Database Password
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
title="Only owners of the workspace can delete apps"
|
||||
visible={!isOwner}
|
||||
hasDisabledChildren={!isOwner}
|
||||
>
|
||||
<Dropdown.Item
|
||||
className={twMerge(
|
||||
'font-display text-sm font-medium text-dark',
|
||||
!isOwner
|
||||
? 'cursor-not-allowed text-red text-opacity-70'
|
||||
: 'font-medium text-red',
|
||||
)}
|
||||
onClick={() => setDeleteApplicationModal(true)}
|
||||
disabled={!isOwner}
|
||||
>
|
||||
<span>Delete Project</span>
|
||||
</Dropdown.Item>
|
||||
</Tooltip>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationMenuItems;
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useInsertFeatureFlagMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserEmail } from '@nhost/react';
|
||||
import { useUserEmail } from '@nhost/nextjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { RemoveApplicationModal } from './RemoveApplicationModal';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { inputErrorMessages } from '@/utils/getErrorMessage';
|
||||
import { slugifyString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useUpdateApplicationMutation } from '@/utils/__generated__/graphql';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export function ChangeApplicationName({ close }: any) {
|
||||
const [updateAppName, { client }] = useUpdateApplicationMutation({});
|
||||
const { workspaceContext } = useWorkspaceContext();
|
||||
const [name, setName] = useState(workspaceContext.appName);
|
||||
const [applicationError, setApplicationError] = useState<any>('');
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const router = useRouter();
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
const slug = slugifyString(name);
|
||||
if (slug.length < 4 || slug.length > 32) {
|
||||
setApplicationError('Slug should be within 4 and 32 characters.');
|
||||
return;
|
||||
}
|
||||
if (!inputErrorMessages(name, setName, setApplicationError, 'project')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateAppName({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
name,
|
||||
slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast('Project name changed');
|
||||
} catch (error) {
|
||||
await discordAnnounce(
|
||||
`Error trying to delete project: ${currentApplication.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
await updateOwnCache(client);
|
||||
await router.push(`/${currentWorkspace.slug}/${slug}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-6 py-6 text-left w-modal">
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h3" component="h2">
|
||||
Change Project Name
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-flow-row gap-2 mt-4">
|
||||
<Input
|
||||
label="New Project Name"
|
||||
id="projectName"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setApplicationError('');
|
||||
}}
|
||||
fullWidth
|
||||
autoFocus
|
||||
helperText={`https://app.nhost.io/${
|
||||
currentWorkspace.slug
|
||||
}/${slugifyString(name)}`}
|
||||
/>
|
||||
|
||||
{applicationError && (
|
||||
<Alert severity="error">{applicationError}</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2 mt-4">
|
||||
<Button type="submit" disabled={applicationError}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={close}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeApplicationName;
|
||||
@@ -30,7 +30,7 @@ function Plan({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="grid items-center justify-between w-full grid-flow-col px-1 my-4"
|
||||
className="my-4 grid w-full grid-flow-col items-center justify-between px-1"
|
||||
onClick={setPlan}
|
||||
tabIndex={-1}
|
||||
>
|
||||
@@ -48,7 +48,7 @@ function Plan({
|
||||
<Text
|
||||
variant="h3"
|
||||
component="p"
|
||||
className="self-center ml-2 font-medium"
|
||||
className="ml-2 self-center font-medium"
|
||||
>
|
||||
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
|
||||
</Text>
|
||||
@@ -143,7 +143,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 text-left w-welcome">
|
||||
<div className="w-welcome p-6 text-left">
|
||||
<Modal
|
||||
showModal={paymentModal}
|
||||
close={closePaymentModal}
|
||||
@@ -189,7 +189,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2 mt-6">
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Button onClick={handleChangePlanClick} disabled={!selectedPlan}>
|
||||
{!selectedPlan && 'Change Plan'}
|
||||
{selectedPlan && isDowngrade && 'Downgrade'}
|
||||
|
||||
@@ -32,9 +32,12 @@ export default function ConnectGithubModal({ close }: ConnectGithubModalProps) {
|
||||
useState<ConnectGithubModalState>('CONNECTING');
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
|
||||
|
||||
const { data, loading, error } = useGetGithubRepositoriesQuery({
|
||||
pollInterval: 2000,
|
||||
});
|
||||
const { data, loading, error, startPolling } =
|
||||
useGetGithubRepositoriesQuery();
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(2000);
|
||||
}, [startPolling]);
|
||||
|
||||
const handleSelectAnotherRepository = () => {
|
||||
setSelectedRepoId(null);
|
||||
@@ -144,7 +147,7 @@ export default function ConnectGithubModal({ close }: ConnectGithubModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
<RetryableErrorBoundary>
|
||||
<div className=" h-import divide-y-1 divide-divide overflow-y-auto border-t-1 border-b-1">
|
||||
<div className="h-import divide-y-1 divide-divide overflow-y-auto border-t-1 border-b-1">
|
||||
{githubRepositoriesToDisplay.map((repo) => (
|
||||
<Repo
|
||||
key={repo.id}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import type { EnvironmentVariableFragment } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
refetchGetEnvironmentVariablesWhereQuery,
|
||||
useDeleteEnvironmentVariableMutation,
|
||||
useUpdateEnvironmentVariableMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type EnvModalProps = {
|
||||
show: boolean;
|
||||
close: VoidFunction;
|
||||
envVar: EnvironmentVariableFragment;
|
||||
};
|
||||
|
||||
export default function EditEnvVarModal({
|
||||
show,
|
||||
close,
|
||||
envVar,
|
||||
}: EnvModalProps) {
|
||||
const { workspaceContext } = useWorkspaceContext();
|
||||
const { appId } = workspaceContext;
|
||||
|
||||
const [updateEnvVar, { loading: updateLoading }] =
|
||||
useUpdateEnvironmentVariableMutation({
|
||||
refetchQueries: [
|
||||
refetchGetEnvironmentVariablesWhereQuery({
|
||||
where: {
|
||||
appId: {
|
||||
_eq: appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const [deleteEnvVar, { loading: deleteLoading }] =
|
||||
useDeleteEnvironmentVariableMutation({
|
||||
refetchQueries: [
|
||||
refetchGetEnvironmentVariablesWhereQuery({
|
||||
where: {
|
||||
appId: {
|
||||
_eq: appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const [prodValue, setProdValue] = useState(envVar.prodValue || '');
|
||||
const [devValue, setDevValue] = useState(envVar.devValue || '');
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await updateEnvVar({
|
||||
variables: {
|
||||
id: envVar.id,
|
||||
environmentVariable: {
|
||||
prodValue,
|
||||
devValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
close();
|
||||
triggerToast('Error updating environment variable');
|
||||
return;
|
||||
}
|
||||
triggerToast(`Environment variable ${envVar.name} updated successfully`);
|
||||
close();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteEnvVar({
|
||||
variables: {
|
||||
id: envVar.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
close();
|
||||
triggerToast('Error deleting environment variable');
|
||||
return;
|
||||
}
|
||||
triggerToast(`Environment variable ${envVar.name} removed successfully`);
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal showModal={show} close={close}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
{envVar.name}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
The default value will be available in all environments, unless
|
||||
you override it. All values are encrypted.
|
||||
</Text>
|
||||
|
||||
<div className="my-2 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
autoFocus
|
||||
disabled
|
||||
autoComplete="off"
|
||||
defaultValue={envVar.name}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="prodValue"
|
||||
label="Production Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={prodValue}
|
||||
onChange={(event) => setProdValue(event.target.value)}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="devValue"
|
||||
label="Development Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={devValue}
|
||||
onChange={(event) => setDevValue(event.target.value)}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={updateLoading}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
loading={deleteLoading}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button onClick={close} variant="outlined" color="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
import ErrorBoundaryFallback from '@/components/common/ErrorBoundaryFallback';
|
||||
import TwilioIcon from '@/components/icons/TwilioIcon';
|
||||
import {
|
||||
useGetSmsSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Button, Input } from '@/ui';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useEffect } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useForm,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function EditSMSSettingsForm({
|
||||
close,
|
||||
isAlreadyEnabled,
|
||||
}: {
|
||||
close: () => void;
|
||||
isAlreadyEnabled: boolean;
|
||||
}) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useFormContext<EditSMSSettingsFormData>();
|
||||
const { control } = useFormContext<EditSMSSettingsFormData>();
|
||||
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
let toastId: string;
|
||||
|
||||
const client = useApolloClient();
|
||||
const isNotCompleted =
|
||||
!watch('accountSID') ||
|
||||
!watch('authToken') ||
|
||||
!watch('messagingServiceSID');
|
||||
|
||||
const handleEditSMSSettings = async (data: EditSMSSettingsFormData) => {
|
||||
try {
|
||||
toastId = showLoadingToast('Updating SMS settings...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authSmsTwilioAccountSid: data.accountSID,
|
||||
authSmsTwilioAuthToken: data.authToken,
|
||||
authSmsTwilioMessagingServiceId: data.messagingServiceSID,
|
||||
authSmsPasswordlessEnabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.refetchQueries({ include: ['getSMSSettings'] });
|
||||
toast.remove(toastId);
|
||||
triggerToast('SMS settings updated successfully.');
|
||||
close();
|
||||
} catch (error) {
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(handleEditSMSSettings)}
|
||||
className="flex w-full flex-col pb-1"
|
||||
autoComplete="off"
|
||||
>
|
||||
{errors &&
|
||||
Object.entries(errors).map(([type, error]) => (
|
||||
<Alert key={type} className="mb-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<div className="flex flex-row place-content-between border-t border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Account SID
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="accountSID"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message:
|
||||
'The Account SID must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="accountSID"
|
||||
placeholder="Account SID"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_]+$/gi.test(value)) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row place-content-between border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Auth Token
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="authToken"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message:
|
||||
'The Auth Token must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="authToken"
|
||||
placeholder="Auth Token"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_/.]+$/gi.test(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row place-content-between border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Messaging Service SID
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="messagingServiceSID"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[+a-zA-Z0-9-_/.]+$/,
|
||||
message:
|
||||
'The Messaging Service SID must either be a valid phone number or contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="messagingServiceSID"
|
||||
required
|
||||
placeholder="Messaging Service SID"
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[+a-zA-Z0-9-_/.]+$/gi.test(value)) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="text-grayscaleDark mt-2 border text-sm+ font-normal"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || isNotCompleted}
|
||||
>
|
||||
{isAlreadyEnabled ? 'Update SMS Settings' : 'Enable SMS'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditSMSSettingsModal({
|
||||
close,
|
||||
isAlreadyEnabled,
|
||||
}: {
|
||||
close: () => void;
|
||||
isAlreadyEnabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-modal px-6 py-4 text-left">
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto mt-2.5">
|
||||
<TwilioIcon className=" text-greyscaleDark" />
|
||||
</div>
|
||||
<Text
|
||||
variant="subHeading"
|
||||
color="greyscaleDark"
|
||||
size="large"
|
||||
className="mt-3 text-center"
|
||||
>
|
||||
Set up Twilio SMS Service
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="small"
|
||||
className="mt-0.5 mb-6 text-center font-normal"
|
||||
>
|
||||
SMS messages are sent through Twilio. Create an account and a
|
||||
messaging service at https://console.twilio.com.
|
||||
</Text>
|
||||
<div>
|
||||
<ErrorBoundary fallbackRender={ErrorBoundaryFallback}>
|
||||
<EditSMSSettingsForm
|
||||
close={close}
|
||||
isAlreadyEnabled={isAlreadyEnabled}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface EditSMSSettingsProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface EditSMSSettingsFormData {
|
||||
accountSID: string;
|
||||
authToken: string;
|
||||
messagingServiceSID: string;
|
||||
}
|
||||
|
||||
export function EditSMSSettings({ close }: EditSMSSettingsProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const form = useForm<EditSMSSettingsFormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
accountSID: '',
|
||||
authToken: '',
|
||||
messagingServiceSID: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetSmsSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue('accountSID', data.app.authSmsTwilioAccountSid);
|
||||
form.setValue('authToken', data.app.authSmsTwilioAuthToken);
|
||||
form.setValue(
|
||||
'messagingServiceSID',
|
||||
data.app.authSmsTwilioMessagingServiceId,
|
||||
);
|
||||
}, [data, form]);
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<EditSMSSettingsModal
|
||||
close={close}
|
||||
isAlreadyEnabled={data.app.authSmsPasswordlessEnabled}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import {
|
||||
useGetSmsSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Text, Toggle } from '@/ui';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function EnableSMSSignIn({ openSMSSettingsModal }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
|
||||
const { data, loading, error } = useGetSmsSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
const [enableSMSLoginMethod, setEnableSMSLoginMethod] = useState(false);
|
||||
const client = useApolloClient();
|
||||
let toastId: string;
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEnableSMSLoginMethod(data.app.authSmsPasswordlessEnabled);
|
||||
}, [data]);
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const handleDisable = async () => {
|
||||
try {
|
||||
toastId = showLoadingToast('Disabling SMS login...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authSmsPasswordlessEnabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
setEnableSMSLoginMethod(false);
|
||||
await client.refetchQueries({ include: ['getSMSSettings'] });
|
||||
toast.remove(toastId);
|
||||
triggerToast('Passwordless SMS disabled.');
|
||||
} catch (updateError) {
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
|
||||
throw updateError;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-20">
|
||||
<div className="mx-auto font-display">
|
||||
<div className="flex flex-col place-content-between">
|
||||
<div className="">
|
||||
<div className="flex flex-row place-content-between">
|
||||
<div className="relative flex flex-row">
|
||||
<Image
|
||||
src="/assets/SMS.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
alt="Phone Number (SMS)"
|
||||
/>
|
||||
<Text
|
||||
variant="body"
|
||||
size="large"
|
||||
className="ml-2 font-medium"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
Phone Number (SMS)
|
||||
</Text>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'ml-2 align-bottom text-sm- font-medium text-blue transition-opacity duration-300',
|
||||
!enableSMSLoginMethod && 'invisible opacity-0',
|
||||
enableSMSLoginMethod && 'opacity-100',
|
||||
)}
|
||||
onClick={() => openSMSSettingsModal()}
|
||||
>
|
||||
Edit SMS settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Toggle
|
||||
checked={enableSMSLoginMethod}
|
||||
onChange={async () => {
|
||||
if (enableSMSLoginMethod) {
|
||||
await handleDisable();
|
||||
} else {
|
||||
openSMSSettingsModal();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row self-center mt-3 align-middle">
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
color="greyscaleDark"
|
||||
className="self-center"
|
||||
>
|
||||
Sign in users with Phone Number (SMS).
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EnableSMSSignIn;
|
||||
@@ -1,11 +1,16 @@
|
||||
import { ConnectionDetail } from '@/components/applications/ConnectionDetail';
|
||||
import { LoadingScreen } from '@/components/common/LoadingScreen';
|
||||
import ExternalLink from '@/components/icons/ExternalIcon';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import generateAppServiceUrl, {
|
||||
defaultLocalBackendSlugs,
|
||||
defaultRemoteBackendSlugs,
|
||||
} from '@/utils/common/generateAppServiceUrl';
|
||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface HasuraDataProps {
|
||||
@@ -14,6 +19,7 @@ interface HasuraDataProps {
|
||||
|
||||
export function HasuraData({ close }: HasuraDataProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
if (
|
||||
!currentApplication?.subdomain ||
|
||||
@@ -23,9 +29,15 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
}
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? process.env.NEXT_PUBLIC_NHOST_HASURA_URL || 'http://localhost:9695'
|
||||
: generateRemoteAppUrl(currentApplication.subdomain);
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||
? `${LOCAL_HASURA_URL}/console`
|
||||
: generateAppServiceUrl(
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region.awsName,
|
||||
'hasura',
|
||||
defaultLocalBackendSlugs,
|
||||
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md px-6 py-4 text-left">
|
||||
@@ -59,14 +71,14 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Link
|
||||
href={`${hasuraUrl}/console`}
|
||||
href={hasuraUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="grid grid-flow-col items-center justify-center gap-1 rounded-[4px] bg-btn p-2 text-sm+ font-medium text-white hover:ring-2 motion-safe:transition-all"
|
||||
underline="none"
|
||||
>
|
||||
Open Hasura
|
||||
<ExternalLink className="ml-0.5 h-4 w-4" />
|
||||
<ArrowSquareOutIcon className="h-4 w-4" />
|
||||
</Link>
|
||||
|
||||
{close && (
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppInjectedVariablesQuery,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
export interface EditCustomUserJWTTokenData {
|
||||
customUserJWTToken: string;
|
||||
}
|
||||
|
||||
export type JWTSecretModalState = 'SHOW' | 'EDIT';
|
||||
|
||||
export interface JWTSecretModalProps {
|
||||
close: () => void;
|
||||
data?: any;
|
||||
jwtSecret: string;
|
||||
initialModalState?: JWTSecretModalState;
|
||||
}
|
||||
|
||||
export function EditJWTSecretModal({ close }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { submitState, setSubmitState } = useSubmitState();
|
||||
|
||||
const [updateApplication] = useUpdateApplicationMutation({
|
||||
refetchQueries: [
|
||||
refetchGetAppInjectedVariablesQuery({ id: currentApplication.id }),
|
||||
],
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<EditCustomUserJWTTokenData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
customUserJWTToken: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleEditJWTSecret = async (data: EditCustomUserJWTTokenData) => {
|
||||
setSubmitState({
|
||||
error: null,
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
try {
|
||||
JSON.parse(data.customUserJWTToken);
|
||||
} catch (error) {
|
||||
setSubmitState({
|
||||
error: new Error('The custom JWT token should be valid json.'),
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
hasuraGraphqlJwtSecret: data.customUserJWTToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
triggerToast(
|
||||
`Successfully added custom JWT token to ${currentApplication.name}.`,
|
||||
);
|
||||
close();
|
||||
} catch (error) {
|
||||
triggerToast(
|
||||
`Error adding custom JWT token to ${currentApplication.name}`,
|
||||
);
|
||||
setSubmitState({ error, loading: false, fieldsWithError: [] });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="px-6 py-4 w-modal"
|
||||
onSubmit={handleSubmit(handleEditJWTSecret)}
|
||||
>
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-row text-left">
|
||||
<Text variant="h3" component="h2">
|
||||
Add Custom JWT Secret
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
You can add your custom JWT key here. Hasura will use this key to
|
||||
validate the identity of your users.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{submitState.error && (
|
||||
<Alert severity="error">{submitState.error.message}</Alert>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="customUserJWTToken"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Paste your custom JWT token here..."
|
||||
componentsProps={{
|
||||
inputRoot: {
|
||||
className: 'font-mono bg-header',
|
||||
},
|
||||
}}
|
||||
aria-label="Custom JWT token"
|
||||
type="text"
|
||||
value={field.value}
|
||||
onBlur={() =>
|
||||
setSubmitState({
|
||||
error: null,
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
})
|
||||
}
|
||||
multiline
|
||||
rows={6}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
|
||||
return (
|
||||
<div className="px-6 py-4 w-modal">
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-row text-left">
|
||||
<Text variant="h3" component="h2">
|
||||
Auth JWT Secret
|
||||
</Text>
|
||||
<Text variant="subtitle2">
|
||||
This is the key used for generating JWTs. It's HMAC-SHA-based
|
||||
and the same as configured in Hasura.
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
defaultValue={JWTKey}
|
||||
multiline
|
||||
disabled
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
rows={6}
|
||||
componentsProps={{
|
||||
inputRoot: { className: 'font-mono' },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-sm mx-auto text-center">
|
||||
<Text variant="subtitle2">
|
||||
Already using a third party auth service? <br />
|
||||
<button
|
||||
type="button"
|
||||
className="mt-0.5 ml-0.5 text-xs font-medium text-blue"
|
||||
onClick={() => {
|
||||
editJWTSecret();
|
||||
}}
|
||||
>
|
||||
Add your custom JWT token
|
||||
</button>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function JWTSecretModal({
|
||||
close,
|
||||
data,
|
||||
jwtSecret,
|
||||
initialModalState,
|
||||
}: any) {
|
||||
const [jwtSecretModalState, setJwtSecretModalState] =
|
||||
useState<JWTSecretModalState>(initialModalState || 'SHOW');
|
||||
|
||||
const editJWTSecret = () => {
|
||||
setJwtSecretModalState('EDIT');
|
||||
};
|
||||
|
||||
if (jwtSecretModalState === 'EDIT') {
|
||||
return <EditJWTSecretModal close={close} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ShowJWTTokenModal
|
||||
JWTKey={jwtSecret || data}
|
||||
editJWTSecret={editJWTSecret}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { triggerToast } from '@/utils/toast';
|
||||
import { useDeleteApplicationMutation } from '@/utils/__generated__/graphql';
|
||||
import router from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface RemoveApplicationModalProps {
|
||||
/**
|
||||
@@ -26,6 +27,10 @@ export interface RemoveApplicationModalProps {
|
||||
* Description of the modal
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Class name to be applied to the modal.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RemoveApplicationModal({
|
||||
@@ -33,6 +38,7 @@ export function RemoveApplicationModal({
|
||||
handler,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
}: RemoveApplicationModalProps) {
|
||||
const [deleteApplication, { client }] = useDeleteApplicationMutation();
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
@@ -72,18 +78,14 @@ export function RemoveApplicationModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-modal text-left">
|
||||
<div className={twMerge('w-full max-w-sm p-6 text-left', className)}>
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
{title || 'Delete Project'}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
{description ? (
|
||||
<div>{description}</div>
|
||||
) : (
|
||||
<div>Are you sure you want to delete this app?</div>
|
||||
)}
|
||||
{description || 'Are you sure you want to delete this app?'}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2" className="font-bold !text-rose-600">
|
||||
@@ -98,33 +100,15 @@ export function RemoveApplicationModal({
|
||||
checked={remove}
|
||||
onChange={(_event, checked) => setRemove(checked)}
|
||||
aria-label="Confirm Delete Project #1"
|
||||
componentsProps={{
|
||||
formControlLabel: {
|
||||
componentsProps: {
|
||||
typography: {
|
||||
className: '!text-sm+',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
id="accept-2"
|
||||
label="I understand this action can not be undone"
|
||||
label="I understand this action cannot be undone"
|
||||
className="py-2"
|
||||
checked={remove2}
|
||||
onChange={(_event, checked) => setRemove2(checked)}
|
||||
aria-label="Confirm Delete Project #2"
|
||||
componentsProps={{
|
||||
formControlLabel: {
|
||||
componentsProps: {
|
||||
typography: {
|
||||
className: '!text-sm+',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ContainerAllWorkspacesApplications } from './ContainerAllWorkspacesAppl
|
||||
|
||||
function ApplicationCreatedAt({ createdAt }: any) {
|
||||
return (
|
||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
||||
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||
created{' '}
|
||||
{formatDistance(new Date(createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
@@ -30,9 +30,9 @@ function LastSuccesfulDeployment({ deployment }: any) {
|
||||
<Avatar
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
className="self-center w-4 h-4 mr-1"
|
||||
className="mr-1 h-4 w-4 self-center"
|
||||
/>
|
||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
||||
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||
{deployment.commitUserName} deployed{' '}
|
||||
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
||||
addSuffix: true,
|
||||
@@ -48,9 +48,9 @@ function CurrentDeployment({ deployment }: any) {
|
||||
<Avatar
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
className="self-center w-4 h-4 mr-1"
|
||||
className="mr-1 h-4 w-4 self-center"
|
||||
/>
|
||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
||||
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||
{deployment.commitUserName} updated just now
|
||||
</Text>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@ export function RenderWorkspacesWithApps({
|
||||
variant="a"
|
||||
color="greyscaleGrey"
|
||||
size="normal"
|
||||
className="mb-3 font-medium cursor-pointer"
|
||||
className="mb-3 cursor-pointer font-medium"
|
||||
>
|
||||
{workspace.name}
|
||||
</Text>
|
||||
@@ -138,16 +138,16 @@ export function RenderWorkspacesWithApps({
|
||||
? app.deployments[0].deploymentStatus === 'DEPLOYING'
|
||||
: false;
|
||||
return (
|
||||
<div key={app.slug} className="py-4 cursor-pointer">
|
||||
<div key={app.slug} className="cursor-pointer py-4">
|
||||
<Link href={`${workspace?.slug}/${app.slug}`} passHref>
|
||||
<a
|
||||
href={`${workspace?.slug}/${app.slug}`}
|
||||
className="flex px-2 bg-white rounded-sm place-content-between border-divide"
|
||||
className="flex place-content-between rounded-sm border-divide bg-white px-2"
|
||||
>
|
||||
<div className="flex flex-col self-center w-full">
|
||||
<div className="flex flex-row w-full place-content-between">
|
||||
<div className="flex w-full flex-col self-center">
|
||||
<div className="flex w-full flex-row place-content-between">
|
||||
<div className="flex flex-row items-center self-center">
|
||||
<div className="w-10 h-10 overflow-hidden rounded-lg">
|
||||
<div className="h-10 w-10 overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
@@ -155,12 +155,12 @@ export function RenderWorkspacesWithApps({
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col ml-2 text-left">
|
||||
<div className="ml-2 flex flex-col text-left">
|
||||
<div>
|
||||
<Text
|
||||
color="dark"
|
||||
size="normal"
|
||||
className="self-center font-medium text-left capitalize cursor-pointer"
|
||||
className="cursor-pointer self-center text-left font-medium capitalize"
|
||||
>
|
||||
{app.name}
|
||||
</Text>
|
||||
@@ -192,7 +192,7 @@ export function RenderWorkspacesWithApps({
|
||||
<div className="flex flex-row">
|
||||
<div className="flex self-center align-middle">
|
||||
{app.deployments[0] && (
|
||||
<div className="flex self-center mr-2 align-middle">
|
||||
<div className="mr-2 flex self-center align-middle">
|
||||
<StatusCircle
|
||||
status={
|
||||
app.deployments[0]
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function SelectedWorkspaceOnNewApp({ current }: any) {
|
||||
const user = nhost.auth.getUser();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row space-x-2 self-center">
|
||||
{current.name === 'Default Workspace' ? (
|
||||
<Avatar
|
||||
className="h-5 w-5 self-center rounded-full"
|
||||
name={user?.displayName}
|
||||
avatarUrl={user?.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-5 w-5 overflow-hidden rounded-md">
|
||||
<Image src="/logos/new.svg" alt="Nhost Logo" width={20} height={20} />
|
||||
</div>
|
||||
)}
|
||||
<Text size="small" color="greyscaleDark" className="font-normal">
|
||||
{current.name}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default SelectedWorkspaceOnNewApp;
|
||||
@@ -13,14 +13,17 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [normalizedFunctionData, setNormalizedFunctionData] = useState(null);
|
||||
|
||||
const { data } = useGetFunctionLogQuery({
|
||||
const { data, startPolling } = useGetFunctionLogQuery({
|
||||
variables: {
|
||||
subdomain: currentApplication.subdomain,
|
||||
functionPaths: [functionName?.split('/').slice(1, 3).join('/')],
|
||||
},
|
||||
pollInterval: 3000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(3000);
|
||||
}, [startPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || data.getFunctionLogs.length === 0) {
|
||||
return;
|
||||
@@ -38,8 +41,8 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
|
||||
normalizedFunctionData.logs.length === 0
|
||||
) {
|
||||
return (
|
||||
<div className="w-full text-white rounded-lg">
|
||||
<div className="px-4 py-4 overflow-auto font-mono rounded-lg shadow-sm h-terminal bg-log">
|
||||
<div className="w-full rounded-lg text-white">
|
||||
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
|
||||
<div className="font-mono text-xs text-grey">
|
||||
There are no stored logs yet. Try calling your function for logs to
|
||||
appear.
|
||||
@@ -50,12 +53,12 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="w-full text-white rounded-lg">
|
||||
<div className="px-4 py-4 overflow-auto font-mono rounded-lg shadow-sm h-terminal bg-log">
|
||||
<div className="w-full rounded-lg text-white">
|
||||
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
|
||||
{normalizedFunctionData.logs.map((log) => (
|
||||
<div
|
||||
key={`${log.date}-${log.message.slice(66)}`}
|
||||
className="flex text-sm "
|
||||
className=" flex text-sm"
|
||||
>
|
||||
<div id={`#-${log.date}`}>
|
||||
<pre className="inline">
|
||||
|
||||
@@ -24,7 +24,7 @@ export function EditRepositorySettings({
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const form = useForm<EditRepositorySettingsFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
productionBranch: currentApplication.repositoryProductionBranch || 'main',
|
||||
repoBaseFolder: currentApplication.nhostBaseFolder,
|
||||
|
||||
@@ -78,8 +78,8 @@ export function EditRepositorySettingsModal({
|
||||
return (
|
||||
<div className="px-1">
|
||||
<div className="flex flex-col">
|
||||
<div className="w-8 h-8 mx-auto">
|
||||
<GithubIcon className="w-8 h-8 text-greyscaleDark" />
|
||||
<div className="mx-auto h-8 w-8">
|
||||
<GithubIcon className="h-8 w-8 text-greyscaleDark" />
|
||||
</div>
|
||||
<Text
|
||||
variant="subHeading"
|
||||
@@ -95,7 +95,7 @@ export function EditRepositorySettingsModal({
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="small"
|
||||
className="font-normal text-center"
|
||||
className="text-center font-normal"
|
||||
>
|
||||
{selectedRepoId
|
||||
? `We'll deploy changes automatically when you push to the deployment branch. `
|
||||
@@ -110,7 +110,7 @@ export function EditRepositorySettingsModal({
|
||||
<div className="">
|
||||
<RepoAndBranch />
|
||||
</div>
|
||||
<div className="flex flex-col mt-2">
|
||||
<div className="mt-2 flex flex-col">
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
@@ -123,7 +123,7 @@ export function EditRepositorySettingsModal({
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex flex-col mt-2">
|
||||
<div className="mt-2 flex flex-col">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ExternalLink from '@/components/icons/ExternalIcon';
|
||||
import GithubIcon from '@/components/icons/GithubIcon';
|
||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function GitHubInstallNhostApplication() {
|
||||
underline="none"
|
||||
>
|
||||
Configure the Nhost application on GitHub{' '}
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<ArrowSquareOutIcon className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,19 +20,19 @@ export function GitHubNoRepositoriesAdded({
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="tiny"
|
||||
className="font-normal text-center"
|
||||
className="text-center font-normal"
|
||||
>
|
||||
Check the Nhost app's settings on your GitHub account, or install
|
||||
the app on a new account.
|
||||
</Text>
|
||||
|
||||
<div className="py-3 my-2 border-t border-b">
|
||||
<div className="my-2 border-t border-b py-3">
|
||||
<div className="flex">
|
||||
{filteredGitHubAppInstallations.map((githubApp) => (
|
||||
<div key={githubApp.id} className="flex items-center mr-4">
|
||||
<div key={githubApp.id} className="mr-4 flex items-center">
|
||||
<Avatar
|
||||
avatarUrl={githubApp.accountAvatarUrl as string}
|
||||
className="w-5 h-5 mr-1"
|
||||
className="mr-1 h-5 w-5"
|
||||
/>
|
||||
{githubApp.accountLogin}
|
||||
</div>
|
||||
@@ -45,9 +45,9 @@ export function GitHubNoRepositoriesAdded({
|
||||
rel="noreferrer noopener"
|
||||
transparent
|
||||
type={null}
|
||||
className="text-xs font-medium cursor-pointer text-blue"
|
||||
className="cursor-pointer text-xs font-medium text-blue"
|
||||
>
|
||||
<PlusSmIcon className="w-4 h-4 mr-1 border rounded-full border-btn" />
|
||||
<PlusSmIcon className="mr-1 h-4 w-4 rounded-full border border-btn" />
|
||||
Configure the Nhost application on GitHub.
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLCl
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import Select from '@/ui/v2/Select';
|
||||
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
||||
import type { RemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import { useRemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import { DEFAULT_ROLES } from './utils';
|
||||
|
||||
@@ -57,7 +57,7 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user: RemoteAppGetUsersQuery['users'][number] = users.find(
|
||||
const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
|
||||
({ id }) => id === userId,
|
||||
);
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './AppDeployments';
|
||||
@@ -1,167 +0,0 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import {
|
||||
useResetPostgresPasswordMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Text } from '@/ui';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import CopyIcon from '@/ui/v2/icons/CopyIcon';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { generateRandomPassword, schema } from '@/utils/generateRandomPassword';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface ResetDatabasePasswordFormProps {
|
||||
/**
|
||||
* The new password to set for the database.
|
||||
*/
|
||||
newDatabasePassword: string;
|
||||
}
|
||||
|
||||
export default function ResetDatabasePasswordForm() {
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
const [updateApplication] = useUpdateApplicationMutation();
|
||||
|
||||
const form = useForm<ResetDatabasePasswordFormProps>({
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
newDatabasePassword: generateRandomPassword(),
|
||||
},
|
||||
});
|
||||
|
||||
const { setValue, getValues, register } = form;
|
||||
|
||||
const { closeAlertDialog } = useDialog();
|
||||
const [resetPostgresPasswordMutation, { loading }] =
|
||||
useResetPostgresPasswordMutation();
|
||||
const user = useUserData();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const handleGenerateRandomPassword = () => {
|
||||
const newRandomDatabasePassword = generateRandomPassword();
|
||||
setPasswordError('');
|
||||
triggerToast('New random database password generated.');
|
||||
setValue('newDatabasePassword', newRandomDatabasePassword);
|
||||
};
|
||||
|
||||
const handleChangeDatabasePassword = async (
|
||||
data: ResetDatabasePasswordFormProps,
|
||||
) => {
|
||||
try {
|
||||
await resetPostgresPasswordMutation({
|
||||
variables: {
|
||||
appID: currentApplication.id,
|
||||
newPassword: data.newDatabasePassword,
|
||||
},
|
||||
});
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
postgresPassword: data.newDatabasePassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
closeAlertDialog();
|
||||
triggerToast(`${currentApplication.name} Database Password changed.`);
|
||||
} catch (e) {
|
||||
triggerToast(
|
||||
`Error trying to change database password for ${currentApplication.name}`,
|
||||
);
|
||||
await discordAnnounce(
|
||||
`Error trying to change database password: ${currentApplication.name} (${user.email}): ${e.message}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form className="mx-0.5" onSubmit={handleChangeDatabasePassword}>
|
||||
<Input
|
||||
{...register('newDatabasePassword')}
|
||||
name="newDatabasePassword"
|
||||
id="newDatabasePassword"
|
||||
autoComplete="new-password"
|
||||
type="password"
|
||||
error={Boolean(passwordError)}
|
||||
helperText={
|
||||
<>
|
||||
{passwordError && <div className="pb-2">{passwordError}</div>}
|
||||
<Text className="font-normal" size="tiny" color="greyscaleDark">
|
||||
The root postgres password for your database - it must be strong
|
||||
and hard to guess.{' '}
|
||||
<Button
|
||||
onClick={handleGenerateRandomPassword}
|
||||
className="contents text-xs "
|
||||
>
|
||||
<span className="ml-1 font-medium text-greyscaleDark underline underline-offset-2">
|
||||
Generate a password
|
||||
</span>
|
||||
</Button>
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
endAdornment={
|
||||
<InputAdornment position="end" className="absolute right-2">
|
||||
<Button
|
||||
sx={{ minWidth: 0, padding: 0 }}
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
copy(getValues('newDatabasePassword'), 'Postgres password');
|
||||
}}
|
||||
variant="borderless"
|
||||
aria-label="Copy your newly randomly generated password to the clipboard."
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</InputAdornment>
|
||||
}
|
||||
onChange={async (e) => {
|
||||
if (e.target.value.length === 0) {
|
||||
setValue('newDatabasePassword', e.target.value);
|
||||
|
||||
setPasswordError('Please enter a password');
|
||||
return;
|
||||
}
|
||||
setValue('newDatabasePassword', e.target.value);
|
||||
setPasswordError('');
|
||||
try {
|
||||
await schema.validate({
|
||||
'Database Password': e.target.value,
|
||||
});
|
||||
setPasswordError('');
|
||||
} catch (validationError) {
|
||||
setPasswordError(validationError.message);
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<div className="mt-6 grid grid-flow-col place-content-between py-2">
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="borderless"
|
||||
onClick={closeAlertDialog}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={Boolean(passwordError)}
|
||||
loading={loading}
|
||||
>
|
||||
Reset Database Password
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './ResetDatabasePasswordForm';
|
||||
export { default } from './ResetDatabasePasswordForm';
|
||||
@@ -1,70 +0,0 @@
|
||||
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
|
||||
|
||||
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
|
||||
// refactor
|
||||
export interface AppleProviderSettingsFormProps {
|
||||
authProviderClientId: string;
|
||||
authProviderTeamId: string;
|
||||
authProviderKeyId: string;
|
||||
authProviderClientSecret: string;
|
||||
handleClientIdChange: (value: string) => void;
|
||||
handleTeamIdChange: (value: string) => void;
|
||||
handleKeyIdChange: (value: string) => void;
|
||||
handleClientSecretChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function AppleProviderSettingsForm({
|
||||
authProviderClientId,
|
||||
authProviderTeamId,
|
||||
authProviderKeyId,
|
||||
authProviderClientSecret,
|
||||
handleClientIdChange,
|
||||
handleTeamIdChange,
|
||||
handleKeyIdChange,
|
||||
handleClientSecretChange,
|
||||
}: AppleProviderSettingsFormProps) {
|
||||
return (
|
||||
<div className="space-y-3 divide-y-1 divide-divide">
|
||||
<ProviderSetting
|
||||
title="Team ID"
|
||||
desc="Copy from Apple and enter here"
|
||||
inputPlaceholder="Paste Team ID here"
|
||||
input
|
||||
inputValue={authProviderTeamId}
|
||||
inputOnChange={handleTeamIdChange}
|
||||
inputType="text"
|
||||
/>
|
||||
|
||||
<ProviderSetting
|
||||
title="Service ID"
|
||||
desc="Copy from Apple and enter here"
|
||||
inputPlaceholder="Paste Service ID here"
|
||||
input
|
||||
inputValue={authProviderClientId}
|
||||
inputOnChange={handleClientIdChange}
|
||||
inputType="text"
|
||||
/>
|
||||
|
||||
<ProviderSetting
|
||||
title="Key ID"
|
||||
desc="Copy from Apple and enter here"
|
||||
inputPlaceholder="Paste Key ID here"
|
||||
input
|
||||
inputValue={authProviderKeyId}
|
||||
inputOnChange={handleKeyIdChange}
|
||||
inputType="text"
|
||||
/>
|
||||
|
||||
<ProviderSetting
|
||||
title="Private Key"
|
||||
desc="Copy from Apple and enter here"
|
||||
inputPlaceholder="Paste Private Key here"
|
||||
input
|
||||
inputValue={authProviderClientSecret.replace(/\\n/gi, '\n')}
|
||||
inputOnChange={handleClientSecretChange}
|
||||
inputType="text"
|
||||
multiline
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './AppleProviderSettingsForm';
|
||||
export { default } from './AppleProviderSettingsForm';
|
||||
@@ -1,44 +0,0 @@
|
||||
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
|
||||
import type { Provider } from '@/types/providers';
|
||||
|
||||
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
|
||||
// refactor
|
||||
export interface GeneralProviderSettingsFormProps {
|
||||
provider: Provider;
|
||||
authProviderClientId: string;
|
||||
authProviderClientSecret: string;
|
||||
handleClientIdChange: (value: string) => void;
|
||||
handleClientSecretChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function GeneralProviderSettingsForm({
|
||||
provider,
|
||||
authProviderClientId,
|
||||
authProviderClientSecret,
|
||||
handleClientIdChange,
|
||||
handleClientSecretChange,
|
||||
}: GeneralProviderSettingsFormProps) {
|
||||
return (
|
||||
<div className="space-y-3 divide-y-1 divide-divide">
|
||||
<ProviderSetting
|
||||
title={`${provider.name} Client ID`}
|
||||
desc={`Copy from ${provider.name} and enter here`}
|
||||
inputPlaceholder="Paste Client ID here"
|
||||
input
|
||||
inputValue={authProviderClientId}
|
||||
inputOnChange={handleClientIdChange}
|
||||
inputType="text"
|
||||
/>
|
||||
|
||||
<ProviderSetting
|
||||
title={`${provider.name} Client Secret`}
|
||||
desc={`Copy from ${provider.name} and enter here`}
|
||||
inputPlaceholder="Paste secret here"
|
||||
input
|
||||
inputValue={authProviderClientSecret}
|
||||
inputOnChange={handleClientSecretChange}
|
||||
inputType="password"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './GeneralProviderSettingsForm';
|
||||
export { default } from './GeneralProviderSettingsForm';
|
||||
@@ -1,20 +0,0 @@
|
||||
import { capitalize } from '@/utils/helpers';
|
||||
import Image from 'next/image';
|
||||
|
||||
export interface PreviewProps {
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export function Preview({ provider }: PreviewProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-10">
|
||||
<Image
|
||||
src={`/assets/social-providers/${provider.toLowerCase()}-preview.svg`}
|
||||
alt={`${capitalize(provider)} sign in preview`}
|
||||
className="mx-auto w-full max-w-md"
|
||||
width={480}
|
||||
height={267}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import Help from '@/components/icons/Help';
|
||||
import type { Provider } from '@/types/providers';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { resolveProvider } from '@/utils/resolveProvider';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type ProviderHeaderProps = {
|
||||
provider: Provider;
|
||||
};
|
||||
|
||||
export function ProviderHeader({ provider }: ProviderHeaderProps) {
|
||||
const router = useRouter();
|
||||
const providerId = router.query.providerId as string;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<div className="w-14">
|
||||
<Image
|
||||
src={`/assets/${resolveProvider(providerId)}.svg`}
|
||||
alt={`Logo of ${provider.name}`}
|
||||
width={56}
|
||||
height={56}
|
||||
layout="responsive"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-row place-content-between">
|
||||
<Text color="dark" className="font-medium capitalize" size="big">
|
||||
{provider.name}
|
||||
</Text>
|
||||
{provider.docsLink && (
|
||||
<div className="flex flex-col">
|
||||
<a href={provider.docsLink} target="_blank" rel="noreferrer">
|
||||
<Help className="h-10 w-10" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProviderHeader;
|
||||
@@ -1,27 +0,0 @@
|
||||
import { useGetAppLoginDataQuery } from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import { ProviderPage } from './ProviderPage';
|
||||
|
||||
export function ProviderPagePreload() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const { data, loading, error } = useGetAppLoginDataQuery({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
},
|
||||
skip: !currentApplication?.id,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator delay={500} label="Loading providers..." />;
|
||||
}
|
||||
|
||||
return <ProviderPage app={data?.app} />;
|
||||
}
|
||||
|
||||
export default ProviderPagePreload;
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Preview } from '@/components/applications/providers/Preview';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useFormSaver } from '@/hooks/useFormSaver';
|
||||
import type { Provider } from '@/types/providers';
|
||||
import { FormSaver } from '@/ui/FormSaver';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Toggle } from '@/ui/Toggle';
|
||||
import { getDynamicVariables } from '@/utils/getDynamicVariables';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useUpdateAppMutation } from '@/utils/__generated__/graphql';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type ProviderInfoProps = {
|
||||
provider: Provider;
|
||||
authProviderEnabled: boolean;
|
||||
setAuthProviderEnabled: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
export function ProviderInfo({
|
||||
provider,
|
||||
authProviderEnabled,
|
||||
setAuthProviderEnabled,
|
||||
}: ProviderInfoProps) {
|
||||
const router = useRouter();
|
||||
const providerId = router.query.providerId as string;
|
||||
const { showFormSaver, setShowFormSaver, submitState } = useFormSaver();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const [updateApp, { client }] = useUpdateAppMutation();
|
||||
|
||||
const { authEnabled } = getDynamicVariables(providerId, {}, true);
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
[authEnabled as string]: authProviderEnabled,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await client.refetchQueries({
|
||||
include: ['getAppLoginData'],
|
||||
});
|
||||
|
||||
setShowFormSaver(false);
|
||||
triggerToast('Settings saved');
|
||||
} catch (error) {
|
||||
// TODO: Display error to user and use a logging solution
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showFormSaver && (
|
||||
<FormSaver
|
||||
show={showFormSaver}
|
||||
onCancel={() => {
|
||||
setShowFormSaver(false);
|
||||
setAuthProviderEnabled(false);
|
||||
}}
|
||||
onSave={() => {
|
||||
handleFormSubmit();
|
||||
}}
|
||||
loading={submitState.loading}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-8 flex flex-row place-content-between">
|
||||
<div className=" space-y-3">
|
||||
<div className="flex flex-col">
|
||||
<Text
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
className=" font-bold"
|
||||
size="normal"
|
||||
>
|
||||
Let users sign in with
|
||||
<span className="ml-1 capitalize">{provider.name}</span>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-center">
|
||||
<Toggle
|
||||
checked={authProviderEnabled}
|
||||
onChange={() => {
|
||||
if (authProviderEnabled) {
|
||||
setShowFormSaver(true);
|
||||
}
|
||||
setAuthProviderEnabled(!authProviderEnabled);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!authProviderEnabled && <Preview provider={providerId} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProviderInfo;
|
||||
@@ -1,45 +0,0 @@
|
||||
import providers from '@/data/providers.json';
|
||||
import { resolveProvider } from '@/utils/resolveProvider';
|
||||
import type { GetAppFragment } from '@/utils/__generated__/graphql';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { ProviderHeader } from './ProviderHeader';
|
||||
import { ProviderInfo } from './ProviderInfo';
|
||||
import { ProviderSettings } from './ProviderSettings';
|
||||
|
||||
type ProviderPageProps = {
|
||||
app: GetAppFragment;
|
||||
};
|
||||
|
||||
export function ProviderPage({ app }: ProviderPageProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const providerId = router.query.providerId as string;
|
||||
|
||||
const providerEnabled = app[`auth${resolveProvider(providerId)}Enabled`];
|
||||
|
||||
const [authProviderEnabled, setAuthProviderEnabled] =
|
||||
useState(providerEnabled);
|
||||
|
||||
const provider = providers.find(
|
||||
(availableProvider) => providerId === availableProvider.name.toLowerCase(),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProviderHeader provider={provider} />
|
||||
<ProviderInfo
|
||||
provider={provider}
|
||||
authProviderEnabled={authProviderEnabled}
|
||||
setAuthProviderEnabled={setAuthProviderEnabled}
|
||||
/>
|
||||
<ProviderSettings
|
||||
provider={provider}
|
||||
app={app}
|
||||
authProviderEnabled={authProviderEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProviderPage;
|
||||
@@ -1,378 +0,0 @@
|
||||
import {
|
||||
ProviderSetting,
|
||||
ProviderSettingsSave,
|
||||
} from '@/components/applications/settings/providers';
|
||||
import type { GetAppFragment } from '@/generated/graphql';
|
||||
import { useUpdateAppMutation } from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useFormSaver } from '@/hooks/useFormSaver';
|
||||
import type { Provider } from '@/types/providers';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { FormSaver } from '@/ui/FormSaver';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ChevronDownIcon from '@/ui/v2/icons/ChevronDownIcon';
|
||||
import ChevronUpIcon from '@/ui/v2/icons/ChevronUpIcon';
|
||||
import { capitalize, generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import { resolveProvider } from '@/utils/resolveProvider';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import AppleProviderSettingsForm from './AppleProviderSettingsForm';
|
||||
import GeneralProviderSettingsForm from './GeneralProviderSettingsForm';
|
||||
import WorkOsProviderSettingsForm from './WorkOsProviderSettingsForm';
|
||||
|
||||
export interface ProviderSettingsProps {
|
||||
provider: Provider;
|
||||
app: GetAppFragment;
|
||||
authProviderEnabled: boolean;
|
||||
}
|
||||
|
||||
// TODO 1: Simplify this component, improve the reusability by redesigning the
|
||||
// way the component renders the content, because it's hard to create a provider
|
||||
// specific layout with the current implementation.
|
||||
|
||||
// TODO 2: Change the form to use react-hook-form, so that we can avoid passing
|
||||
// too much props around these components (e.g: passing xy and handleXyChange to
|
||||
// children would not be necessary at all).
|
||||
|
||||
// TODO 3: This is an accessibility improvement, but labels should be connected
|
||||
// to the inputs.
|
||||
export function ProviderSettings({
|
||||
provider,
|
||||
app,
|
||||
authProviderEnabled,
|
||||
}: ProviderSettingsProps) {
|
||||
const router = useRouter();
|
||||
const providerId = router.query.providerId as string;
|
||||
const [hideSettings, setHideSettings] = useState(false);
|
||||
const [hasSettings, setHasSettings] = useState(false);
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const {
|
||||
authClientId,
|
||||
authClientSecret,
|
||||
authTeamId,
|
||||
authKeyId,
|
||||
authDefaultDomain,
|
||||
authDefaultOrganization,
|
||||
authDefaultConnection,
|
||||
// TODO: This function should be extracted from this component and also it
|
||||
// should be checked why values are used from it's return value **inside**
|
||||
// the function body.
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
} = getProviderSpecificVariables(providerId);
|
||||
|
||||
const [authProviderClientSecret, setAuthProviderClientSecret] = useState(
|
||||
app[authClientSecret] || '',
|
||||
);
|
||||
|
||||
const [authProviderClientId, setAuthProviderClientId] = useState(
|
||||
app[authClientId] || '',
|
||||
);
|
||||
|
||||
const [authProviderTeamId, setAuthProviderTeamId] = useState(
|
||||
app[authTeamId] || '',
|
||||
);
|
||||
|
||||
const [authProviderKeyId, setAuthProviderKeyId] = useState(
|
||||
app[authKeyId] || '',
|
||||
);
|
||||
|
||||
const [authProviderDefaultDomain, setAuthProviderDefaultDomain] = useState(
|
||||
app[authDefaultDomain] || '',
|
||||
);
|
||||
const [authProviderDefaultOrganization, setAuthProviderDefaultOrganization] =
|
||||
useState(app[authDefaultOrganization] || '');
|
||||
const [authProviderDefaultConnection, setAuthProviderDefaultConnection] =
|
||||
useState(app[authDefaultConnection] || '');
|
||||
|
||||
const [callError, setCallError] = useState({ error: false, message: '' });
|
||||
|
||||
const [updateApp, { client, loading }] = useUpdateAppMutation();
|
||||
const { showFormSaver, setShowFormSaver, submitState } = useFormSaver();
|
||||
|
||||
function getProviderSpecificVariables(
|
||||
targetProvider: string,
|
||||
{ prefill = true } = {},
|
||||
) {
|
||||
if (targetProvider === 'twitter') {
|
||||
if (!prefill) {
|
||||
return {
|
||||
authTwitterEnabled: authProviderEnabled,
|
||||
authTwitterConsumerKey: authProviderClientId,
|
||||
authTwitterConsumerSecret: authProviderClientSecret,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authEnabled: 'authTwitterEnabled',
|
||||
authClientId: 'authTwitterConsumerKey',
|
||||
authClientSecret: 'authTwitterConsumerSecret',
|
||||
};
|
||||
}
|
||||
|
||||
if (targetProvider === 'apple') {
|
||||
if (!prefill) {
|
||||
return {
|
||||
authAppleEnabled: authProviderEnabled,
|
||||
authAppleClientId: authProviderClientId,
|
||||
authAppleKeyId: authProviderKeyId,
|
||||
authAppleTeamId: authProviderTeamId,
|
||||
authApplePrivateKey: authProviderClientSecret.replace(/\n/gi, '\\n'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authEnabled: 'authAppleEnabled',
|
||||
authClientId: 'authAppleClientId',
|
||||
authClientSecret: 'authApplePrivateKey',
|
||||
authTeamId: 'authAppleTeamId',
|
||||
authKeyId: 'authAppleKeyId',
|
||||
};
|
||||
}
|
||||
|
||||
if (targetProvider === 'workos') {
|
||||
if (!prefill) {
|
||||
return {
|
||||
authWorkOsEnabled: authProviderEnabled,
|
||||
authWorkOsClientId: authProviderClientId,
|
||||
authWorkOsClientSecret: authProviderClientSecret,
|
||||
authWorkOsDefaultDomain: authProviderDefaultDomain,
|
||||
authWorkOsDefaultOrganization: authProviderDefaultOrganization,
|
||||
authWorkOsDefaultConnection: authProviderDefaultConnection,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authEnabled: 'authWorkOsEnabled',
|
||||
authClientId: 'authWorkOsClientId',
|
||||
authClientSecret: 'authWorkOsClientSecret',
|
||||
authDefaultDomain: 'authWorkOsDefaultDomain',
|
||||
authDefaultOrganization: 'authWorkOsDefaultOrganization',
|
||||
authDefaultConnection: 'authWorkOsDefaultConnection',
|
||||
};
|
||||
}
|
||||
|
||||
const authEnabled = `auth${resolveProvider(providerId)}Enabled`;
|
||||
const clientId = `auth${resolveProvider(providerId)}ClientId`;
|
||||
const clientSecret = `auth${resolveProvider(providerId)}ClientSecret`;
|
||||
|
||||
if (!prefill) {
|
||||
return {
|
||||
[authEnabled]: authProviderEnabled,
|
||||
[clientId]: authProviderClientId,
|
||||
[clientSecret]: authProviderClientSecret,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authEnabled,
|
||||
authClientId: clientId,
|
||||
authClientSecret: clientSecret,
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Gets the particular providerId GQL field.
|
||||
const { authEnabled } = getProviderSpecificVariables(providerId);
|
||||
// Checks if the providerId field is enabled on the app that we get from origin.
|
||||
if (app[authEnabled]) {
|
||||
setHasSettings(true);
|
||||
setHideSettings(true);
|
||||
}
|
||||
}, [hasSettings, setHasSettings, app]);
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
authClientId: clientId,
|
||||
authTeamId: teamId,
|
||||
authKeyId: keyId,
|
||||
authClientSecret: clientSecret,
|
||||
} = getProviderSpecificVariables(providerId);
|
||||
|
||||
// This side effect checks if the clientId or secret doesn't equal the app's clientId or secret and shows the form saver, which can be used to save the new changes.
|
||||
if (
|
||||
hasSettings &&
|
||||
(app[clientSecret] !== authProviderClientSecret ||
|
||||
app[clientId] !== authProviderClientId ||
|
||||
app[teamId] !== authProviderTeamId ||
|
||||
app[keyId] !== authProviderKeyId)
|
||||
) {
|
||||
setShowFormSaver(true);
|
||||
}
|
||||
}, [
|
||||
hasSettings,
|
||||
authProviderClientSecret,
|
||||
authProviderClientId,
|
||||
authProviderTeamId,
|
||||
authProviderKeyId,
|
||||
authClientSecret,
|
||||
authClientId,
|
||||
]);
|
||||
|
||||
const handleSettingsToggle = (e) => {
|
||||
e.preventDefault();
|
||||
setHideSettings(!hideSettings);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e?: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
...getProviderSpecificVariables(providerId, { prefill: false }),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
setCallError({ error: true, message: error.message });
|
||||
return;
|
||||
}
|
||||
await client.refetchQueries({
|
||||
include: ['getAppLoginData'],
|
||||
});
|
||||
setShowFormSaver(false);
|
||||
triggerToast('Settings saved');
|
||||
};
|
||||
|
||||
const handleClientIdChange = (value: string) => {
|
||||
setCallError({ error: false, message: '' });
|
||||
setAuthProviderClientId(value);
|
||||
};
|
||||
|
||||
const handleTeamIdChange = (value: string) => {
|
||||
setCallError({ error: false, message: '' });
|
||||
setAuthProviderTeamId(value);
|
||||
};
|
||||
|
||||
const handleKeyIdChange = (value: string) => {
|
||||
setCallError({ error: false, message: '' });
|
||||
setAuthProviderKeyId(value);
|
||||
};
|
||||
|
||||
const handleClientSecretChange = (value: string) => {
|
||||
setCallError({ error: false, message: '' });
|
||||
setAuthProviderClientSecret(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showFormSaver && (
|
||||
<FormSaver
|
||||
show={showFormSaver}
|
||||
onCancel={() => {
|
||||
setShowFormSaver(false);
|
||||
}}
|
||||
onSave={() => {
|
||||
handleSubmit();
|
||||
}}
|
||||
loading={submitState.loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{authProviderEnabled && (
|
||||
<div className="mt-8 space-y-3 divide-y-1 divide-divide border-t border-b pb-2">
|
||||
{!hideSettings && (
|
||||
<>
|
||||
{providerId === 'apple' && (
|
||||
<AppleProviderSettingsForm
|
||||
authProviderClientId={authProviderClientId}
|
||||
authProviderTeamId={authProviderTeamId}
|
||||
authProviderKeyId={authProviderKeyId}
|
||||
authProviderClientSecret={authProviderClientSecret}
|
||||
handleClientIdChange={handleClientIdChange}
|
||||
handleTeamIdChange={handleTeamIdChange}
|
||||
handleKeyIdChange={handleKeyIdChange}
|
||||
handleClientSecretChange={handleClientSecretChange}
|
||||
/>
|
||||
)}
|
||||
{providerId !== 'apple' && (
|
||||
<GeneralProviderSettingsForm
|
||||
provider={provider}
|
||||
authProviderClientId={authProviderClientId}
|
||||
authProviderClientSecret={authProviderClientSecret}
|
||||
handleClientIdChange={handleClientIdChange}
|
||||
handleClientSecretChange={handleClientSecretChange}
|
||||
/>
|
||||
)}
|
||||
{providerId === 'workos' && (
|
||||
<WorkOsProviderSettingsForm
|
||||
defaultDomain={authProviderDefaultDomain}
|
||||
defaultOrganization={authProviderDefaultOrganization}
|
||||
defaultConnection={authProviderDefaultConnection}
|
||||
handleDefaultDomainChange={setAuthProviderDefaultDomain}
|
||||
handleDefaultOrganizationChange={
|
||||
setAuthProviderDefaultOrganization
|
||||
}
|
||||
handleDefaultConnectionChange={
|
||||
setAuthProviderDefaultConnection
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ProviderSetting
|
||||
title={hideSettings ? 'Login button URL' : 'OAuth Callback URL'}
|
||||
desc={
|
||||
hideSettings
|
||||
? `Use this in your frontend`
|
||||
: `Paste into ${capitalize(providerId)}`
|
||||
}
|
||||
inputPlaceholder=""
|
||||
input={false}
|
||||
showCopy
|
||||
link={
|
||||
hideSettings
|
||||
? `${generateRemoteAppUrl(
|
||||
app.subdomain,
|
||||
)}/v1/auth/signin/provider/${providerId.toLowerCase()}`
|
||||
: `${generateRemoteAppUrl(
|
||||
app.subdomain,
|
||||
)}/v1/auth/signin/provider/${providerId.toLowerCase()}/callback`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{callError.error && (
|
||||
<Alert severity="error">
|
||||
{callError.message ||
|
||||
'Error trying to update login provider settings.'}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{authProviderEnabled && hasSettings && (
|
||||
<div className="mt-4 px-2">
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={handleSettingsToggle}
|
||||
className="grid grid-flow-col gap-1.5 text-xs"
|
||||
>
|
||||
{hideSettings ? (
|
||||
<>
|
||||
View Settings
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Hide Settings
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authProviderEnabled && !hasSettings && (
|
||||
<ProviderSettingsSave provider={provider} loading={loading} />
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
|
||||
|
||||
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
|
||||
// refactor
|
||||
export interface WorkOsProviderSettingsFormProps {
|
||||
defaultDomain: string;
|
||||
defaultOrganization: string;
|
||||
defaultConnection: string;
|
||||
handleDefaultDomainChange: (value: string) => void;
|
||||
handleDefaultOrganizationChange: (value: string) => void;
|
||||
handleDefaultConnectionChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function WorkOsProviderSettingsForm({
|
||||
defaultDomain,
|
||||
defaultOrganization,
|
||||
defaultConnection,
|
||||
handleDefaultDomainChange,
|
||||
handleDefaultOrganizationChange,
|
||||
handleDefaultConnectionChange,
|
||||
}: WorkOsProviderSettingsFormProps) {
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-3 divide-y-1">
|
||||
<ProviderSetting
|
||||
title="Default Domain"
|
||||
desc=""
|
||||
inputPlaceholder=""
|
||||
input
|
||||
inputValue={defaultDomain}
|
||||
inputOnChange={handleDefaultDomainChange}
|
||||
inputType="text"
|
||||
/>
|
||||
|
||||
<ProviderSetting
|
||||
title="Default Organization"
|
||||
desc=""
|
||||
inputPlaceholder=""
|
||||
input
|
||||
inputValue={defaultOrganization}
|
||||
inputOnChange={handleDefaultOrganizationChange}
|
||||
inputType="text"
|
||||
/>
|
||||
|
||||
<ProviderSetting
|
||||
title="Default Connection"
|
||||
desc=""
|
||||
inputPlaceholder=""
|
||||
input
|
||||
inputValue={defaultConnection}
|
||||
inputOnChange={handleDefaultConnectionChange}
|
||||
inputType="text"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './WorkOsProviderSettingsForm';
|
||||
export { default } from './WorkOsProviderSettingsForm';
|
||||
@@ -1,138 +0,0 @@
|
||||
import { PermissionSetting } from '@/components/applications/users/PermissionSetting';
|
||||
import { SettingsSection } from '@/components/applications/users/SettingsSection';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
useGetAuthSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function GeneralPermissions() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
const client = useApolloClient();
|
||||
const { submitState, setSubmitState } = useSubmitState();
|
||||
let toastId: string;
|
||||
|
||||
const { loading, data, error } = useGetAuthSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full bg-white ">
|
||||
<SettingsSection
|
||||
title="General Permissions"
|
||||
desc="These settings affect all users in your project."
|
||||
>
|
||||
{submitState.error && (
|
||||
<Alert severity="error">{submitState.error.message}</Alert>
|
||||
)}
|
||||
<div className="divide-y-1 border-t border-b">
|
||||
<PermissionSetting
|
||||
text="Disable New Users"
|
||||
desc="If set, newly registered users are disabled and won't be able to sign in."
|
||||
toggle
|
||||
checked={data.app.authDisableNewUsers}
|
||||
onChange={async () => {
|
||||
try {
|
||||
toastId = showLoadingToast('Saving changes...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authDisableNewUsers: !data.app.authDisableNewUsers,
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.refetchQueries({ include: ['getAuthSettings'] });
|
||||
toast.remove(toastId);
|
||||
triggerToast(
|
||||
`Disable new users ${
|
||||
data.app.authDisableNewUsers ? `Disabled` : `Enabled`
|
||||
} for ${currentApplication.name}`,
|
||||
);
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
triggerToast(updateError.message);
|
||||
}
|
||||
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
|
||||
setSubmitState({
|
||||
loading: false,
|
||||
error: updateError,
|
||||
fieldsWithError: ['authDisableNewUsers'],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PermissionSetting
|
||||
text="Allow Anonymous Users"
|
||||
desc="Enables users to register as an anonymous user."
|
||||
toggle
|
||||
checked={data.app.authAnonymousUsersEnabled}
|
||||
onChange={async () => {
|
||||
setSubmitState({
|
||||
loading: true,
|
||||
error: null,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
try {
|
||||
toastId = showLoadingToast('Saving changes...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authAnonymousUsersEnabled:
|
||||
!data.app.authAnonymousUsersEnabled,
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.refetchQueries({ include: ['getAuthSettings'] });
|
||||
toast.remove(toastId);
|
||||
triggerToast(
|
||||
`Anonymous users registration ${
|
||||
data.app.authAnonymousUsersEnabled ? `disabled` : `enabled`
|
||||
} for ${currentApplication.name}`,
|
||||
);
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
triggerToast(updateError.message);
|
||||
}
|
||||
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
|
||||
setSubmitState({
|
||||
loading: false,
|
||||
error: updateError,
|
||||
fieldsWithError: ['authAnonymousUsersEnabled'],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GeneralPermissions;
|
||||
@@ -1,263 +0,0 @@
|
||||
import { PermissionSetting } from '@/components/applications/users/PermissionSetting';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
||||
import { Toggle } from '@/ui';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
useGetGravatarSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function GravatarSettings() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
const client = useApolloClient();
|
||||
const [currentDefaultGravatar, setCurrentDefaultGravatar] = useState({
|
||||
id: 'blank',
|
||||
name: 'blank',
|
||||
disabled: false,
|
||||
slug: 'blank',
|
||||
});
|
||||
const [currentGravatarRating, setCurrentGravatarRating] = useState({
|
||||
id: 'g',
|
||||
name: 'g',
|
||||
disabled: false,
|
||||
slug: 'g',
|
||||
});
|
||||
|
||||
const { submitState, setSubmitState } = useSubmitState();
|
||||
|
||||
const { loading, data, error } = useGetGravatarSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
let toastId: string;
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentDefaultGravatar((previousDefaultGravatar) => ({
|
||||
...previousDefaultGravatar,
|
||||
name: data.app.authGravatarDefault,
|
||||
id: data.app.authGravatarDefault,
|
||||
slug: data.app.authGravatarDefault,
|
||||
}));
|
||||
|
||||
setCurrentGravatarRating((previousGravatarRating) => ({
|
||||
...previousGravatarRating,
|
||||
name: data.app.authGravatarRating,
|
||||
id: data.app.authGravatarRating,
|
||||
slug: data.app.authGravatarRating,
|
||||
}));
|
||||
}, [data, setCurrentDefaultGravatar, setCurrentGravatarRating]);
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full font-display">
|
||||
<div className="flex flex-row place-content-between">
|
||||
<div className="flex flex-col">
|
||||
<Text
|
||||
variant="body"
|
||||
size="large"
|
||||
className="font-medium"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
Gravatar Settings
|
||||
</Text>
|
||||
<div>
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
color="greyscaleDark"
|
||||
className="mt-1"
|
||||
>
|
||||
Enable Gravatars as avatar URL for users.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2 flex flex-row">
|
||||
<Toggle
|
||||
checked={data.app.authGravatarEnabled}
|
||||
onChange={async () => {
|
||||
try {
|
||||
toastId = showLoadingToast('Saving changes...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authGravatarEnabled: !data.app.authGravatarEnabled,
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.refetchQueries({
|
||||
include: ['getGravatarSettings'],
|
||||
});
|
||||
toast.remove(toastId);
|
||||
triggerToast(
|
||||
`Gravatars ${
|
||||
data.app.authGravatarEnabled ? `Disabled` : `Enabled`
|
||||
} for ${currentApplication.name}`,
|
||||
);
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
triggerToast(updateError.message);
|
||||
}
|
||||
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
|
||||
setSubmitState({
|
||||
loading: false,
|
||||
error: updateError,
|
||||
fieldsWithError: ['authGravatarEnabled'],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submitState.error && (
|
||||
<Alert severity="error" className="mt-4">
|
||||
{submitState.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{data.app.authGravatarEnabled && (
|
||||
<div className="mt-6 mb-12 flex flex-col divide-y-1 divide-divide border-t border-b">
|
||||
<PermissionSetting
|
||||
text="AUTH_GRAVATAR_DEFAULT"
|
||||
options={[
|
||||
{
|
||||
id: '404',
|
||||
name: '404',
|
||||
},
|
||||
{
|
||||
id: 'mp',
|
||||
name: 'mp',
|
||||
},
|
||||
{
|
||||
id: 'identicon',
|
||||
name: 'identicon',
|
||||
},
|
||||
{
|
||||
id: 'monsterid',
|
||||
name: 'monsterid',
|
||||
},
|
||||
{
|
||||
id: 'waatar',
|
||||
name: 'waatar',
|
||||
},
|
||||
{
|
||||
id: 'retro',
|
||||
name: 'retro',
|
||||
},
|
||||
{
|
||||
id: 'robohash',
|
||||
name: 'robohash',
|
||||
},
|
||||
{
|
||||
id: 'blank',
|
||||
name: 'blank',
|
||||
},
|
||||
]}
|
||||
value={currentDefaultGravatar}
|
||||
onChange={async (v: { id: string }) => {
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authGravatarDefault: v.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
client.refetchQueries({ include: ['getGravatarSettings'] });
|
||||
triggerToast(
|
||||
`Changed default gravatar for ${currentApplication.name}`,
|
||||
);
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
triggerToast(updateError.message);
|
||||
}
|
||||
|
||||
setSubmitState({
|
||||
loading: false,
|
||||
error: updateError,
|
||||
fieldsWithError: ['authGravatarDefault'],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PermissionSetting
|
||||
text="AUTH_GRAVATAR_RATING"
|
||||
options={[
|
||||
{
|
||||
id: 'g',
|
||||
name: 'g',
|
||||
},
|
||||
{
|
||||
id: 'pg',
|
||||
name: 'pg',
|
||||
},
|
||||
{
|
||||
id: 'r',
|
||||
name: 'r',
|
||||
},
|
||||
{
|
||||
id: 'x',
|
||||
name: 'x',
|
||||
},
|
||||
]}
|
||||
value={currentGravatarRating}
|
||||
onChange={async (v: { id: string }) => {
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authGravatarRating: v.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
client.refetchQueries({ include: ['getGravatarSettings'] });
|
||||
triggerToast(
|
||||
`Changed Gravatar rating for ${currentApplication.name}`,
|
||||
);
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
triggerToast(updateError.message);
|
||||
}
|
||||
|
||||
setSubmitState({
|
||||
loading: false,
|
||||
error: updateError,
|
||||
fieldsWithError: ['authGravatarRating'],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GravatarSettings;
|
||||
@@ -1,193 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useFormSaver } from '@/hooks/useFormSaver';
|
||||
import { FormSaver, Toggle } from '@/ui';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
useGetAuthSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function MultiFactorAuthentication() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
const [OTPIssuer, setOTPIssuer] = useState('');
|
||||
const client = useApolloClient();
|
||||
const { showFormSaver, setShowFormSaver, submitState, setSubmitState } =
|
||||
useFormSaver();
|
||||
|
||||
const toastId = useRef<string>();
|
||||
|
||||
const { loading, data, error } = useGetAuthSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.app.authMfaTotpIssuer) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOTPIssuer(data.app.authMfaTotpIssuer);
|
||||
}, [data]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
label="Loading settings..."
|
||||
className="mx-auto"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSaveForm() {
|
||||
setSubmitState({
|
||||
loading: true,
|
||||
error: null,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authMfaTotpIssuer: OTPIssuer,
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.refetchQueries({ include: ['getAuthSettings'] });
|
||||
setSubmitState({
|
||||
loading: false,
|
||||
error: null,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
setShowFormSaver(false);
|
||||
triggerToast('All changes saved');
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
triggerToast(updateError.message);
|
||||
}
|
||||
|
||||
setSubmitState({
|
||||
loading: false,
|
||||
error: updateError,
|
||||
fieldsWithError: ['OTPIssuer'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleMFA() {
|
||||
try {
|
||||
toastId.current = showLoadingToast('Saving changes...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authMfaEnabled: !data.app.authMfaEnabled,
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.refetchQueries({ include: ['getAuthSettings'] });
|
||||
|
||||
if (toastId?.current) {
|
||||
toast.remove(toastId.current);
|
||||
}
|
||||
|
||||
triggerToast(
|
||||
`Multi-Factor Authentication ${
|
||||
data.app.authMfaEnabled ? `Disabled` : `Enabled`
|
||||
} for ${currentApplication.name}`,
|
||||
);
|
||||
} catch (updateError) {
|
||||
if (toastId?.current) {
|
||||
toast.remove(toastId.current);
|
||||
}
|
||||
|
||||
if (updateError instanceof Error) {
|
||||
triggerToast(updateError.message);
|
||||
}
|
||||
|
||||
setSubmitState({
|
||||
loading: false,
|
||||
error: updateError,
|
||||
fieldsWithError: ['authMfaEnabled'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid w-full grid-flow-row gap-4">
|
||||
{showFormSaver && (
|
||||
<FormSaver
|
||||
show={showFormSaver}
|
||||
onCancel={() => {
|
||||
setShowFormSaver(false);
|
||||
}}
|
||||
onSave={handleSaveForm}
|
||||
loading={submitState.loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row place-content-between">
|
||||
<div className="grid grid-flow-row gap-1.5">
|
||||
<Text variant="h3" component="h2">
|
||||
Multi-Factor Authentication
|
||||
</Text>
|
||||
<Text>Enable users to use multi-factor authentication (MFA).</Text>
|
||||
</div>
|
||||
|
||||
<div className="mr-2 flex flex-row">
|
||||
<Toggle
|
||||
checked={data.app.authMfaEnabled}
|
||||
onChange={handleToggleMFA}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submitState.error && (
|
||||
<Alert severity="error">{submitState.error.message}</Alert>
|
||||
)}
|
||||
|
||||
{data.app.authMfaEnabled && (
|
||||
<div className="border-t border-b border-gray-200 py-4">
|
||||
<Input
|
||||
id="otpIssuer"
|
||||
label="Name of the One Time Password (OTP) issuer"
|
||||
onChange={(e) => {
|
||||
setShowFormSaver(true);
|
||||
setOTPIssuer(e.target.value);
|
||||
}}
|
||||
variant="inline"
|
||||
value={OTPIssuer}
|
||||
error={submitState.fieldsWithError?.includes('OTPIssuer')}
|
||||
placeholder={currentApplication.name}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
inlineInputProportion="50%"
|
||||
componentsProps={{
|
||||
label: { className: 'text-sm+' },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultiFactorAuthentication;
|
||||
@@ -1,122 +0,0 @@
|
||||
import Copy from '@/components/icons/Copy';
|
||||
import type { Provider } from '@/types/providers';
|
||||
import { Button } from '@/ui/Button';
|
||||
import CheckBoxes from '@/ui/Checkboxes';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { copy } from '@/utils/copy';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
// TODO: Instead of a `helpers.tsx`, we should have designated files for these
|
||||
// components
|
||||
type ProviderSettingsProps = {
|
||||
title: string;
|
||||
desc: string;
|
||||
inputPlaceholder?: string;
|
||||
input: boolean;
|
||||
inputValue?: string;
|
||||
inputOnChange?: (v: string) => void;
|
||||
inputType?: 'text' | 'password';
|
||||
multiline?: boolean;
|
||||
link?: string;
|
||||
showCopy?: boolean;
|
||||
};
|
||||
|
||||
export function ProviderSetting({
|
||||
title,
|
||||
desc,
|
||||
inputPlaceholder,
|
||||
input,
|
||||
inputValue,
|
||||
inputOnChange,
|
||||
inputType,
|
||||
link,
|
||||
showCopy = false,
|
||||
multiline,
|
||||
}: ProviderSettingsProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full flex-row items-center justify-between px-2 pt-3 pb-1',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-80 flex-col">
|
||||
<Text
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="normal"
|
||||
className="font-medium capitalize"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text color="greyscaleDark" size="tiny" className="font-normal">
|
||||
{desc}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full flex-row place-content-between self-center">
|
||||
{input ? (
|
||||
<Input
|
||||
placeholder={inputPlaceholder || ''}
|
||||
className="h-full w-full"
|
||||
type={inputType}
|
||||
value={inputValue}
|
||||
onChange={inputOnChange}
|
||||
multiline={multiline}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-row self-center align-middle">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
size="tiny"
|
||||
className="self-center font-normal"
|
||||
>
|
||||
{link}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{showCopy && (
|
||||
<Copy
|
||||
className="ml-1 mr-4 h-4 w-4 cursor-pointer self-center text-greyscaleDark"
|
||||
onClick={() => {
|
||||
copy(link as string, title);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ProviderSettingsSaveProps = {
|
||||
provider: Provider;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export function ProviderSettingsSave({
|
||||
provider,
|
||||
loading,
|
||||
}: ProviderSettingsSaveProps) {
|
||||
const [confirmed, setConfirmed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex w-full flex-row place-content-between px-2">
|
||||
<CheckBoxes
|
||||
id="confirm-paste"
|
||||
state={confirmed}
|
||||
setState={() => setConfirmed(!confirmed)}
|
||||
checkBoxText={`I have pasted the redirect URI into ${provider.name}.`}
|
||||
/>
|
||||
<div />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!confirmed}
|
||||
loading={loading}
|
||||
className="self-center"
|
||||
>
|
||||
Confirm Settings
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './helpers';
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { SelectorOption } from '@/ui/Selector';
|
||||
import Selector from '@/ui/Selector';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Toggle } from '@/ui/Toggle';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface PermissionSettingsProps {
|
||||
text: string;
|
||||
desc?: string;
|
||||
toggle?: boolean;
|
||||
onChange?: any;
|
||||
checked?: boolean;
|
||||
options?: any;
|
||||
value?: SelectorOption;
|
||||
} // @TODO: Fix alt attribute on images.
|
||||
// @FIX: Double border
|
||||
|
||||
export function PermissionSetting({
|
||||
text,
|
||||
desc,
|
||||
toggle,
|
||||
checked = false,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
}: PermissionSettingsProps) {
|
||||
return (
|
||||
<div className="flex flex-row place-content-between py-2">
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col space-y-1 self-center px-0.5',
|
||||
!desc && 'py-3.5',
|
||||
desc && 'py-2',
|
||||
)}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
className="font-medium"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
{desc && (
|
||||
<Text
|
||||
variant="body"
|
||||
size="tiny"
|
||||
className="font-normal"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
{desc}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{toggle ? (
|
||||
<div className="flex flex-row">
|
||||
<Toggle checked={checked} onChange={onChange} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row self-center">
|
||||
<Selector
|
||||
width="w-28"
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { Provider as ProviderType } from '@/types/providers';
|
||||
import Status, { StatusEnum } from '@/ui/Status';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
interface ProviderProps {
|
||||
provider: ProviderType;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export function Provider({ provider, enabled }: ProviderProps) {
|
||||
const { name, logo } = provider;
|
||||
|
||||
const {
|
||||
query: { workspaceSlug, appSlug },
|
||||
} = useRouter();
|
||||
|
||||
const nameLowerCase = name.toLowerCase();
|
||||
return (
|
||||
<Link
|
||||
href={`/${workspaceSlug}/${appSlug}/settings/sign-in-methods/${nameLowerCase}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
href={`${workspaceSlug}/${appSlug}/settings/sign-in-methods/${nameLowerCase}`}
|
||||
className="flex cursor-pointer flex-row place-content-between border-t py-2.5"
|
||||
>
|
||||
<div className="grid grid-flow-col items-center gap-2">
|
||||
<div className="h-6 w-6">
|
||||
<Image
|
||||
src={logo}
|
||||
alt={`Logo of ${name}`}
|
||||
width={24}
|
||||
height={24}
|
||||
layout="responsive"
|
||||
/>
|
||||
</div>
|
||||
<Text className="font-medium" color="greyscaleDark" size="normal">
|
||||
{name}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
{enabled ? (
|
||||
<Status status={StatusEnum.Live}>Enabled</Status>
|
||||
) : (
|
||||
<Status status={StatusEnum.Closed}>Disabled</Status>
|
||||
)}
|
||||
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default Provider;
|
||||
@@ -1,183 +0,0 @@
|
||||
import { CreateUserRoleModal } from '@/components/applications/users/roles/CreateRoleModal';
|
||||
import { EditUserRoleModal } from '@/components/applications/users/roles/EditUserRoleModal';
|
||||
import Lock from '@/components/icons/Lock';
|
||||
import type { GetRolesQuery } from '@/generated/graphql';
|
||||
import { Modal } from '@/ui';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import clsx from 'clsx';
|
||||
import type { Dispatch, MouseEvent, MouseEventHandler } from 'react';
|
||||
import { useReducer } from 'react';
|
||||
|
||||
function RolesTableHead() {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-64 py-3 text-left font-medium text-base">
|
||||
<Text className="text-xs font-bold text-greyscaleDark">Role</Text>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
interface UserRoleProps {
|
||||
role: string;
|
||||
isSystemRole: boolean;
|
||||
onClick?: MouseEventHandler<HTMLTableRowElement>;
|
||||
}
|
||||
|
||||
function UserRole({ role, isSystemRole, onClick }: UserRoleProps) {
|
||||
return (
|
||||
<tr
|
||||
className={clsx(isSystemRole ? 'cursor-not-allowed' : 'cursor-pointer')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<td className="py-2">
|
||||
<Text
|
||||
size="normal"
|
||||
className={clsx(
|
||||
isSystemRole ? 'text-greyscaleGrey' : 'text-greyscaleDark',
|
||||
'pl-1 font-medium',
|
||||
)}
|
||||
>
|
||||
{role}
|
||||
</Text>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{isSystemRole ? (
|
||||
<div className="inline-flex pr-1">
|
||||
<Text
|
||||
size="tiny"
|
||||
className=" font-mono text-xs font-medium uppercase tracking-wide text-greyscaleGrey"
|
||||
>
|
||||
System Role
|
||||
</Text>
|
||||
<Lock className="ml-1 h-5 w-5 text-greyscaleGrey" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex self-center py-2 pr-1.5">
|
||||
<ChevronRightIcon className="h-4.5 w-4.5 text-greyscaleDark" />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export type UserRoleDetails = {
|
||||
name: string;
|
||||
isSystemRole: boolean;
|
||||
};
|
||||
|
||||
export const getUserRoles = (data): UserRoleDetails[] => {
|
||||
const authUserDefaultAllowedRoles =
|
||||
data.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
return authUserDefaultAllowedRoles.map((role: string) => ({
|
||||
name: role,
|
||||
isSystemRole: ['user', 'me'].includes(role),
|
||||
}));
|
||||
};
|
||||
|
||||
type ModalState = {
|
||||
visible: boolean;
|
||||
type: 'create' | 'edit';
|
||||
payload: UserRoleDetails;
|
||||
};
|
||||
|
||||
type ModalAction = {
|
||||
type: 'OPEN_CREATE_MODAL' | 'OPEN_EDIT_MODAL' | 'CLOSE_MODAL';
|
||||
payload?: UserRoleDetails;
|
||||
};
|
||||
|
||||
function modalStateReducer(state: ModalState, action: ModalAction): ModalState {
|
||||
switch (action.type) {
|
||||
case 'OPEN_CREATE_MODAL':
|
||||
return { ...state, visible: true, type: 'create', payload: null };
|
||||
case 'OPEN_EDIT_MODAL':
|
||||
return { ...state, visible: true, type: 'edit', payload: action.payload };
|
||||
case 'CLOSE_MODAL':
|
||||
return { ...state, visible: false };
|
||||
default:
|
||||
throw new Error(`Action type ${action.type} is not supported.`);
|
||||
}
|
||||
}
|
||||
|
||||
function AddNewUserRole({ dispatch }: { dispatch: Dispatch<ModalAction> }) {
|
||||
return (
|
||||
<tr className="cursor-pointer border-y-1 border-solid border-gray-300">
|
||||
<td className="p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: 'OPEN_CREATE_MODAL' })}
|
||||
>
|
||||
<Text className="text-sm+ font-medium text-blue">
|
||||
Create New Role
|
||||
</Text>
|
||||
</button>
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function RolesTableBody({ data }: { data: GetRolesQuery }) {
|
||||
const userRoles = getUserRoles(data);
|
||||
const [
|
||||
{ visible: modalVisible, type: modalType, payload: modalPayload },
|
||||
dispatch,
|
||||
] = useReducer(modalStateReducer, {
|
||||
visible: false,
|
||||
type: null,
|
||||
payload: null,
|
||||
});
|
||||
|
||||
function handleRoleEdit(event: MouseEvent<HTMLTableRowElement>, role: any) {
|
||||
dispatch({ type: 'OPEN_EDIT_MODAL', payload: role });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={modalVisible}
|
||||
close={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
>
|
||||
{modalType === 'create' ? (
|
||||
<CreateUserRoleModal
|
||||
onClose={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
/>
|
||||
) : (
|
||||
<EditUserRoleModal
|
||||
onClose={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
<tbody className="divide-y-1 border-t-1 border-b-1 border-solid border-gray-300 ">
|
||||
{userRoles.map((role) => (
|
||||
<UserRole
|
||||
key={role.name}
|
||||
role={role.name}
|
||||
isSystemRole={role.isSystemRole}
|
||||
onClick={
|
||||
role.isSystemRole
|
||||
? undefined
|
||||
: (event) => handleRoleEdit(event, role)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<AddNewUserRole dispatch={dispatch} />
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RolesTable({ data }: { data: GetRolesQuery }) {
|
||||
return (
|
||||
<table className="w-full table-fixed overflow-x-auto">
|
||||
<RolesTableHead />
|
||||
<RolesTableBody data={data} />
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { TextProps } from '@/ui/Text';
|
||||
import { Text } from '@/ui/Text';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface SettingsSectionProps {
|
||||
/**
|
||||
* Title of this section.
|
||||
*/
|
||||
title: ReactNode;
|
||||
/**
|
||||
* Props to be passed to the title component.
|
||||
*/
|
||||
titleProps?: TextProps;
|
||||
/**
|
||||
* Props to be passed to the wrapper component.
|
||||
*/
|
||||
wrapperProps?: TextProps;
|
||||
/**
|
||||
* Description of this section.
|
||||
*/
|
||||
desc?: ReactNode;
|
||||
/**
|
||||
* Props to be passed to the description component.
|
||||
*/
|
||||
descriptionProps?: TextProps;
|
||||
}
|
||||
|
||||
export function SettingsSection({
|
||||
children,
|
||||
title,
|
||||
titleProps,
|
||||
descriptionProps,
|
||||
desc,
|
||||
wrapperProps,
|
||||
}: PropsWithChildren<SettingsSectionProps>) {
|
||||
const { className: titleClassName, ...restTitleProps } = titleProps || {};
|
||||
const { className: wrapperClassName } = wrapperProps || {};
|
||||
const { className: descriptionClassName, ...restDescriptionProps } =
|
||||
descriptionProps || {};
|
||||
|
||||
return (
|
||||
<div className={twMerge('mt-10', wrapperClassName)}>
|
||||
<div className="mx-auto font-display">
|
||||
<div className="flex flex-col place-content-between">
|
||||
<div>
|
||||
<Text
|
||||
size="large"
|
||||
variant="heading"
|
||||
className={twMerge('mb-1.5 font-medium', titleClassName)}
|
||||
color="greyscaleDark"
|
||||
{...restTitleProps}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{desc && (
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
color="greyscaleDark"
|
||||
className={twMerge('mb-3 font-normal', descriptionClassName)}
|
||||
{...restDescriptionProps}
|
||||
>
|
||||
{desc}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,25 +32,25 @@ function Users({ users }: any) {
|
||||
key={user.id}
|
||||
passHref
|
||||
>
|
||||
<tr className="cursor-pointer w-52">
|
||||
<td className="py-1 pr-6 whitespace-nowrap">
|
||||
<tr className="w-52 cursor-pointer">
|
||||
<td className="whitespace-nowrap py-1 pr-6">
|
||||
<div className="flex items-center">
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="p-1 mr-2"
|
||||
className="mr-2 p-1"
|
||||
aria-label="Copy user ID"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
copy(user.id, `User ID`);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
|
||||
<div className="flex-shrink-0 w-8 h-8">
|
||||
<div className="h-8 w-8 flex-shrink-0">
|
||||
<Avatar
|
||||
className="w-8 h-8"
|
||||
className="h-8 w-8"
|
||||
avatarUrl={user.avatarUrl}
|
||||
name={user.displayName}
|
||||
/>
|
||||
@@ -63,7 +63,7 @@ function Users({ users }: any) {
|
||||
<Text
|
||||
variant="a"
|
||||
color="greyscaleDark"
|
||||
className="font-medium cursor-pointer"
|
||||
className="cursor-pointer font-medium"
|
||||
size="normal"
|
||||
>
|
||||
{user.displayName ||
|
||||
@@ -78,7 +78,7 @@ function Users({ users }: any) {
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<td className="whitespace-nowrap px-6 py-4">
|
||||
<Text color="greyscaleDark" className="font-normal" size="normal">
|
||||
{format(new Date(user.createdAt), 'd MMM yyyy')}
|
||||
</Text>
|
||||
@@ -103,13 +103,13 @@ function Users({ users }: any) {
|
||||
);
|
||||
})}
|
||||
</td>
|
||||
<td className="py-4 pl-6 text-sm font-medium text-right whitespace-nowrap">
|
||||
<td className="whitespace-nowrap py-4 pl-6 text-right text-sm font-medium">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/${appSlug}/users/${user.id}`}
|
||||
passHref
|
||||
>
|
||||
<a href={`${workspaceSlug}/${appSlug}/users/${user.id}`}>
|
||||
<ChevronRightIcon className="self-center w-4 h-4 ml-2 cursor-pointer" />
|
||||
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
|
||||
</a>
|
||||
</Link>
|
||||
</td>
|
||||
@@ -131,7 +131,7 @@ function UserPages({ totalNrOfPages, setCurrentPage }: any) {
|
||||
<button
|
||||
type="button"
|
||||
key={i}
|
||||
className="px-2 cursor-pointer"
|
||||
className="cursor-pointer px-2"
|
||||
onClick={() => {
|
||||
setCurrentPage(i);
|
||||
}}
|
||||
@@ -212,15 +212,15 @@ export function UsersTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mt-2 font-display">
|
||||
<div className="inline-block min-w-full py-2 align-">
|
||||
<div className="mt-2 flex flex-col font-display">
|
||||
<div className="align- inline-block min-w-full py-2">
|
||||
<div className="overflow-hidden border-b">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-4 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
||||
className="px-4 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||
>
|
||||
{data ? (
|
||||
<TotalUsers
|
||||
@@ -244,7 +244,7 @@ export function UsersTable({
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
||||
className="px-6 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||
>
|
||||
<Text size="tiny" color="greyscaleDark" className="font-bold">
|
||||
Signed up at
|
||||
@@ -253,7 +253,7 @@ export function UsersTable({
|
||||
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
||||
className="px-6 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||
>
|
||||
<Text size="tiny" color="greyscaleDark" className="font-bold">
|
||||
Roles
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import useCustomClaims from '@/hooks/useCustomClaims';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type {
|
||||
CreatePermissionVariableBaseFormData,
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
} from './CreatePermissionVariableModalBase';
|
||||
import CreatePermissionVariableModalBase from './CreatePermissionVariableModalBase';
|
||||
|
||||
export type CreatePermissionVariableFormData =
|
||||
CreatePermissionVariableBaseFormData;
|
||||
|
||||
export type CreatePermissionVariableModalProps = Pick<
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
'onClose'
|
||||
>;
|
||||
|
||||
export default function CreatePermissionVariableModal({
|
||||
onClose,
|
||||
}: CreatePermissionVariableModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
const form = useForm<CreatePermissionVariableFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
});
|
||||
|
||||
const {
|
||||
workspaceContext: { appId },
|
||||
} = useWorkspaceContext();
|
||||
|
||||
const { data: customClaims } = useCustomClaims({ appId });
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetAppCustomClaimsQuery({ id: appId })],
|
||||
});
|
||||
|
||||
async function handleSubmit(permissionVariable: CustomClaim) {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
if (
|
||||
customClaims.some(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === permissionVariable.key.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Permission variable with this field name already exists.',
|
||||
);
|
||||
}
|
||||
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: [...customClaims, permissionVariable]
|
||||
.filter((claim) => !claim.system)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast('Permission variable created');
|
||||
|
||||
if (!onClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<CreatePermissionVariableModalBase
|
||||
title="Create Permission Variable"
|
||||
type="create"
|
||||
onSubmit={handleSubmit}
|
||||
onClose={onClose}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import type { ChangeEvent, MouseEventHandler, ReactNode } from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface CreatePermissionVariableBaseFormData {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CreateModalBaseProps<T> {
|
||||
/**
|
||||
* Title of this modal.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Type of this modal.
|
||||
*/
|
||||
type?: 'create' | 'edit';
|
||||
/**
|
||||
* Callback to be called when the modal is closed.
|
||||
*/
|
||||
onClose?: VoidFunction;
|
||||
/**
|
||||
* Callback to be called when remove button is clicked.
|
||||
*/
|
||||
onRemove?: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Callback to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit: SubmitHandler<T>;
|
||||
/**
|
||||
* Error to be displayed.
|
||||
*/
|
||||
errorComponent?: ReactNode;
|
||||
}
|
||||
|
||||
export type CreatePermissionVariableModalBaseProps =
|
||||
CreateModalBaseProps<CreatePermissionVariableBaseFormData>;
|
||||
|
||||
export default function CreatePermissionVariableModalBase({
|
||||
title,
|
||||
type,
|
||||
onClose,
|
||||
onRemove,
|
||||
onSubmit,
|
||||
errorComponent,
|
||||
}: CreatePermissionVariableModalBaseProps) {
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
register,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useFormContext<CreatePermissionVariableBaseFormData>();
|
||||
|
||||
const keyHandlers = register('key', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z-]+$/i,
|
||||
message: 'Must contain only letters and hyphens',
|
||||
},
|
||||
});
|
||||
|
||||
const valueHandlers = register('value', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9._[\]]+$/i,
|
||||
message: 'Must contain only letters, dots, brackets, and underscores',
|
||||
},
|
||||
});
|
||||
|
||||
const isComplete = !!watch('key') && !!watch('value');
|
||||
|
||||
return (
|
||||
<div className="w-modal p-6 text-left">
|
||||
<div className="grid w-full grid-flow-col items-center justify-between">
|
||||
<Text variant="h3" component="h2">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{type === 'edit' && onRemove && (
|
||||
<Button variant="borderless" color="error" onClick={onRemove}>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text className="mt-2 text-sm+ text-greyscaleDark">
|
||||
Enter the field name and the path you want to use in this permission
|
||||
variable.
|
||||
</Text>
|
||||
|
||||
{errorComponent}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<div className="my-4 grid grid-flow-row divide-y-1 divide-solid divide-gray-200 border-y border-gray-200">
|
||||
<Input
|
||||
{...keyHandlers}
|
||||
value={watch('key')}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z-]+$/gi.test(event.target.value)
|
||||
) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
keyHandlers.onChange(event);
|
||||
}}
|
||||
id="key"
|
||||
variant="inline"
|
||||
inlineInputProportion="66%"
|
||||
label="Field name"
|
||||
fullWidth
|
||||
startAdornment={
|
||||
<Text className="min-w-[73px] text-sm+ text-greyscaleGrey">
|
||||
X-Hasura-
|
||||
</Text>
|
||||
}
|
||||
componentsProps={{
|
||||
inputWrapper: { className: 'my-1' },
|
||||
input: {
|
||||
className: 'border-transparent focus-within:border-solid pl-2',
|
||||
},
|
||||
inputRoot: { className: '!pl-[1px]' },
|
||||
}}
|
||||
autoFocus
|
||||
error={!!errors?.key?.message}
|
||||
helperText={errors?.key?.message}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...valueHandlers}
|
||||
value={watch('value')}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z-]+$/gi.test(event.target.value)
|
||||
) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
valueHandlers.onChange(event);
|
||||
}}
|
||||
id="value"
|
||||
variant="inline"
|
||||
inlineInputProportion="66%"
|
||||
label="Path"
|
||||
fullWidth
|
||||
startAdornment={
|
||||
<Text className="text-sm+ text-greyscaleGrey">user.</Text>
|
||||
}
|
||||
componentsProps={{
|
||||
inputWrapper: { className: 'my-1' },
|
||||
input: {
|
||||
className: 'border-transparent focus-within:border-solid pl-2',
|
||||
},
|
||||
inputRoot: { className: '!pl-[1px]' },
|
||||
}}
|
||||
error={!!errors?.value?.message}
|
||||
helperText={errors?.value?.message}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || !isComplete}
|
||||
>
|
||||
{type === 'create' ? 'Create Permission Variable' : 'Save Changes'}
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import useCustomClaims from '@/hooks/useCustomClaims';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type {
|
||||
CreatePermissionVariableBaseFormData,
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
} from './CreatePermissionVariableModalBase';
|
||||
import CreatePermissionVariableModalBase from './CreatePermissionVariableModalBase';
|
||||
|
||||
export type EditPermissionVariableFormData =
|
||||
CreatePermissionVariableBaseFormData;
|
||||
|
||||
export type EditPermissionVariableModalProps = Pick<
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
'onClose'
|
||||
> & {
|
||||
/**
|
||||
* The permission variable to edit.
|
||||
*/
|
||||
payload: CustomClaim;
|
||||
};
|
||||
|
||||
export default function EditPermissionVariableModal({
|
||||
payload: originalCustomClaim,
|
||||
...props
|
||||
}: EditPermissionVariableModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
|
||||
const form = useForm<EditPermissionVariableFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
defaultValues: {
|
||||
key: originalCustomClaim.key || '',
|
||||
value: originalCustomClaim.value || '',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
workspaceContext: { appId },
|
||||
} = useWorkspaceContext();
|
||||
|
||||
const { data: customClaims } = useCustomClaims({ appId });
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetAppCustomClaimsQuery({ id: appId })],
|
||||
});
|
||||
|
||||
async function handleSubmit(permissionVariable: CustomClaim) {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
if (
|
||||
originalCustomClaim.key.toLowerCase() !==
|
||||
permissionVariable.key.toLowerCase() &&
|
||||
customClaims.some(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === permissionVariable.key.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Permission variable with this field name already exists.',
|
||||
);
|
||||
}
|
||||
|
||||
// we need to preserve the original position of the permission variable
|
||||
const currentIndex = customClaims.findIndex(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === originalCustomClaim.key.toLowerCase(),
|
||||
);
|
||||
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: customClaims
|
||||
.slice(0, currentIndex)
|
||||
.concat(permissionVariable)
|
||||
.concat(customClaims.slice(currentIndex + 1))
|
||||
.filter((claim) => !claim.system)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast(`Permission variable updated`);
|
||||
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove() {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: customClaims
|
||||
.filter(
|
||||
(claim) =>
|
||||
claim.key !== originalCustomClaim.key && !claim.system,
|
||||
)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setShowRemoveModal(false);
|
||||
|
||||
triggerToast('Permission variable removed');
|
||||
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={showRemoveModal}
|
||||
close={() => setShowRemoveModal(false)}
|
||||
>
|
||||
<div className="grid w-96 grid-flow-row gap-2 p-6 text-left text-greyscaleDark">
|
||||
<Text variant="h3" component="h2">
|
||||
Remove {originalCustomClaim.key}?
|
||||
</Text>
|
||||
|
||||
<Text>You will not be able to use it in permissions anymore.</Text>
|
||||
|
||||
<Text>
|
||||
If you have permission checks currently using this property, they
|
||||
will never resolve to true.
|
||||
</Text>
|
||||
|
||||
<div className="mt-2 grid grid-flow-row gap-2">
|
||||
<Button color="error" onClick={handleRemove} className="w-full">
|
||||
Remove Permission Variable
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => setShowRemoveModal(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CreatePermissionVariableModalBase
|
||||
title="Edit Permission Variable"
|
||||
type="edit"
|
||||
onSubmit={handleSubmit}
|
||||
onRemove={() => setShowRemoveModal(true)}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Loading from '@/ui/Loading';
|
||||
import {
|
||||
refetchGetRolesQuery,
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import type {
|
||||
CreateUserRoleBaseFormData,
|
||||
CreateUserRoleModalBaseProps,
|
||||
} from './CreateUserRoleModalBase';
|
||||
import { CreateUserRoleModalBase } from './CreateUserRoleModalBase';
|
||||
|
||||
export type CreateUserRoleFormData = CreateUserRoleBaseFormData;
|
||||
|
||||
export type CreateUserRoleModalProps = Pick<
|
||||
CreateUserRoleModalBaseProps,
|
||||
'onClose'
|
||||
>;
|
||||
|
||||
export function CreateUserRoleModal({ onClose }: CreateUserRoleModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const form = useForm<CreateUserRoleBaseFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetRolesQuery({ id: currentApplication.id })],
|
||||
});
|
||||
|
||||
const {
|
||||
data: currentRolesData,
|
||||
loading,
|
||||
error: getRolesError,
|
||||
} = useGetRolesQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (getRolesError) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2.5xl">
|
||||
<Alert severity="error">{error.message}</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(data) {
|
||||
setError(undefined);
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = `${currentRolesData.app.authUserDefaultAllowedRoles},${data.roleName}`;
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles: newAuthUserDefaultAllowedRoles,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!onClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<CreateUserRoleModalBase
|
||||
title="Create New Role"
|
||||
type="create"
|
||||
onSubmit={handleSubmit}
|
||||
onClose={onClose}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { CreateModalBaseProps } from '@/components/applications/users/permissions/modal/CreatePermissionVariableModalBase';
|
||||
import { Input } from '@/ui';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface CreateUserRoleBaseFormData {
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
export type CreateUserRoleModalBaseProps =
|
||||
CreateModalBaseProps<CreateUserRoleBaseFormData>;
|
||||
export type CreateUserRoleModal = Pick<CreateUserRoleModalBaseProps, 'onClose'>;
|
||||
|
||||
export function CreateUserRoleModalBase({
|
||||
title,
|
||||
type,
|
||||
onRemove,
|
||||
onSubmit,
|
||||
errorComponent,
|
||||
}: CreateUserRoleModalBaseProps) {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useFormContext<CreateUserRoleBaseFormData>();
|
||||
|
||||
return (
|
||||
<div className="w-modal- p-6 text-left">
|
||||
<div className="mx-auto items-center justify-between">
|
||||
<Text
|
||||
variant="heading"
|
||||
className="text-center text-lg font-medium text-greyscaleDark"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{errorComponent}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<div className="mt-3 mb-3 divide-y border-t border-b py-1">
|
||||
<div className="flex flex-row place-content-between py-2">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
New Role Name
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="roleName"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message: 'Must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="roleName"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_]+$/gi.test(value)) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{type === 'create' ? 'Create New User Role' : 'Save Changes'}
|
||||
</Button>
|
||||
{type === 'edit' && onRemove && (
|
||||
<Button variant="menu" border onClick={onRemove}>
|
||||
<Text className="text-sm+ font-medium text-red">Remove Role</Text>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import type { GetRolesQuery } from '@/generated/graphql';
|
||||
import {
|
||||
refetchGetRolesQuery,
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { Button } from '@/ui/Button';
|
||||
import Loading from '@/ui/Loading';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import type {
|
||||
CreateUserRoleBaseFormData,
|
||||
CreateUserRoleModalBaseProps,
|
||||
} from './CreateUserRoleModalBase';
|
||||
import { CreateUserRoleModalBase } from './CreateUserRoleModalBase';
|
||||
|
||||
export type EditUserRoleFormData = CreateUserRoleBaseFormData;
|
||||
|
||||
export type EditUserRoleModalProps = Pick<
|
||||
CreateUserRoleModalBaseProps,
|
||||
'onClose'
|
||||
> & {
|
||||
/**
|
||||
* The permission variable to edit.
|
||||
*/
|
||||
payload: any;
|
||||
};
|
||||
|
||||
export function EditUserRoleModal({
|
||||
payload: originalRole,
|
||||
...props
|
||||
}: EditUserRoleModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const form = useForm<EditUserRoleFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
defaultValues: {
|
||||
roleName: originalRole.name || '',
|
||||
},
|
||||
});
|
||||
|
||||
const [updateApp, { loading: loadingUpdateAppMutation }] =
|
||||
useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetRolesQuery({ id: currentApplication.id })],
|
||||
});
|
||||
|
||||
const {
|
||||
data: currentRolesData,
|
||||
loading,
|
||||
error: getRolesError,
|
||||
} = useGetRolesQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (getRolesError) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2.5xl">
|
||||
<Alert severity="error">{error.message}</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(data: EditUserRoleFormData) {
|
||||
setError(undefined);
|
||||
|
||||
const currentUserRoles =
|
||||
currentRolesData.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
const roleBeingEdited = currentUserRoles.find(
|
||||
(role) => role === originalRole.name,
|
||||
);
|
||||
|
||||
const indexofRoleBeingEdited = currentUserRoles.indexOf(roleBeingEdited);
|
||||
const newRoleName = data.roleName;
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = currentUserRoles.slice();
|
||||
|
||||
if (data.roleName !== originalRole.name) {
|
||||
newAuthUserDefaultAllowedRoles[indexofRoleBeingEdited] = newRoleName;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles:
|
||||
newAuthUserDefaultAllowedRoles.join(','),
|
||||
},
|
||||
},
|
||||
});
|
||||
triggerToast(`Role "${data.roleName}" updated successfully`);
|
||||
props.onClose();
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove(data: GetRolesQuery) {
|
||||
setError(undefined);
|
||||
|
||||
// Get the current roles of this application.
|
||||
const currentUserRoles = data.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
// Remove the role from the current roles.
|
||||
const filteredCurrentUserRoles = currentUserRoles.filter(
|
||||
(role) => role !== originalRole.name,
|
||||
);
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = filteredCurrentUserRoles.join(',');
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles: newAuthUserDefaultAllowedRoles,
|
||||
},
|
||||
},
|
||||
});
|
||||
props.onClose();
|
||||
triggerToast(`Role "${originalRole.name}" removed successfully`);
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={showRemoveModal}
|
||||
close={() => setShowRemoveModal(false)}
|
||||
>
|
||||
<div className="px-6 pt-5 text-center text-greyscaleDark">
|
||||
<Text variant="heading" className="mb-2 text-lg font-medium">
|
||||
Remove Role "{originalRole.name}"?
|
||||
</Text>
|
||||
|
||||
<div className="my-4">
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => handleRemove(currentRolesData)}
|
||||
className="w-full"
|
||||
loading={loadingUpdateAppMutation}
|
||||
>
|
||||
Remove Role
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowRemoveModal(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CreateUserRoleModalBase
|
||||
title="Edit Role"
|
||||
type="edit"
|
||||
onSubmit={handleSubmit}
|
||||
onRemove={() => setShowRemoveModal(true)}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useState } from 'react';
|
||||
|
||||
type EnvModalProps = {
|
||||
onSubmit: (props: {
|
||||
name: string;
|
||||
prodValue: string;
|
||||
devValue: string;
|
||||
}) => Promise<void>;
|
||||
name?: string;
|
||||
prodValue?: string;
|
||||
devValue?: string;
|
||||
close: VoidFunction;
|
||||
};
|
||||
|
||||
interface AddEnvVarModalVariablesError {
|
||||
hasError: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const DISABLED_START_ENV_VARIABLES = [
|
||||
'NHOST_',
|
||||
'HASURA_',
|
||||
'AUTH_',
|
||||
'STORAGE_',
|
||||
'POSTGRES_',
|
||||
];
|
||||
|
||||
const DISABLED_ENV_VARIABLES = [
|
||||
'PATH',
|
||||
'NODE_PATH',
|
||||
'PYTHONPATH',
|
||||
'GEM_PATH',
|
||||
'HOSTNAME',
|
||||
'TERM',
|
||||
'NODE_VERSION',
|
||||
'YARN_VERSION',
|
||||
'NODE_ENV',
|
||||
'HOME',
|
||||
];
|
||||
|
||||
export default function AddEnvVarModal({
|
||||
name: externalName,
|
||||
prodValue: externalProdValue,
|
||||
devValue: externalDevValue,
|
||||
close,
|
||||
onSubmit,
|
||||
}: EnvModalProps) {
|
||||
const [name, setName] = useState(externalName || '');
|
||||
const [prodValue, setProdValue] = useState(externalProdValue || '');
|
||||
const [devValue, setDevValue] = useState(externalDevValue || '');
|
||||
const [error, setError] = useState<AddEnvVarModalVariablesError>({
|
||||
hasError: false,
|
||||
message: '',
|
||||
});
|
||||
|
||||
const noError: AddEnvVarModalVariablesError = {
|
||||
hasError: false,
|
||||
message: '',
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
setError({ hasError: false, message: '' });
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
DISABLED_START_ENV_VARIABLES.some((envVar) =>
|
||||
name.toUpperCase().startsWith(envVar),
|
||||
)
|
||||
) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The environment variable name cannot start with a value that is reserved for an internal environment variable.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
DISABLED_ENV_VARIABLES.some((envVar) => envVar === name.toUpperCase())
|
||||
) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The environment variable name cannot be a value that is reserved for internal use.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// only allow alphabet characters and underscores
|
||||
const onlyLettersWithNumbersStartsWithLetter = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
|
||||
if (!onlyLettersWithNumbersStartsWithLetter.test(name)) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The name contains invalid characters. Only letters, digits, and underscores are allowed. Furthermore, the name should start with a letter.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
setError({ hasError: true, message: 'Variable name is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prodValue) {
|
||||
setError({ hasError: true, message: 'Production value is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!devValue) {
|
||||
setError({ hasError: true, message: 'Development value is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
await onSubmit({
|
||||
name,
|
||||
prodValue,
|
||||
devValue,
|
||||
});
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
{name || 'EXAMPLE_NAME'}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
The default value will be available in all environments, unless you
|
||||
override it. All values are encrypted.
|
||||
</Text>
|
||||
|
||||
<div className="my-2 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
fullWidth
|
||||
placeholder="EXAMPLE_NAME"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setName(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="prodValue"
|
||||
label="Production Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={prodValue}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setProdValue(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="devValue"
|
||||
label="Development Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={devValue}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setDevValue(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error.hasError && (
|
||||
<Alert severity="warning" className="mb-2">
|
||||
<Text className="font-medium">Warning</Text>
|
||||
<Text>{error.message}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit">Add</Button>
|
||||
|
||||
<Button onClick={close} variant="outlined" color="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -146,7 +146,7 @@ function AddPaymentMethodForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-6 pb-6 text-left w-modal2">
|
||||
<div className="w-modal2 px-6 pt-6 pb-6 text-left">
|
||||
<div className="flex flex-col">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Text
|
||||
@@ -161,11 +161,11 @@ function AddPaymentMethodForm({
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="small"
|
||||
className="font-normal text-center"
|
||||
className="text-center font-normal"
|
||||
>
|
||||
We'll store these in your workspace for future use.
|
||||
</Text>
|
||||
<div className="w-full px-2 py-2 my-2 mt-6 rounded-lg border-1">
|
||||
<div className="my-2 mt-6 w-full rounded-lg border-1 px-2 py-2">
|
||||
<CardElement
|
||||
onReady={(element) => element.focus()}
|
||||
options={{
|
||||
|
||||
@@ -37,20 +37,27 @@ function ControlledAutocomplete(
|
||||
}: ControlledAutocompleteProps<AutocompleteOption>,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
const { setValue } = useFormContext();
|
||||
const form = useFormContext();
|
||||
const { field } = useController({
|
||||
...controllerProps,
|
||||
...(controllerProps || {}),
|
||||
name: controllerProps?.name || name || '',
|
||||
control: controllerProps?.control || control,
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
throw new Error('ControlledAutocomplete must be used in a FormContext.');
|
||||
}
|
||||
|
||||
const { setValue } = form || {};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
{...props}
|
||||
{...field}
|
||||
inputValue={typeof field.value === 'string' ? field.value : undefined}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, options, reason, details) => {
|
||||
setValue(controllerProps?.name || name, options);
|
||||
setValue?.(controllerProps?.name || name, options);
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event, options, reason, details);
|
||||
|
||||
@@ -53,7 +53,7 @@ function ControlledCheckbox(
|
||||
name={field.name}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, checked) => {
|
||||
setValue(controllerProps?.name || name, checked);
|
||||
setValue(controllerProps?.name || name, checked, { shouldDirty: true });
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event, checked);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { SwitchProps } from '@/ui/v2/Switch';
|
||||
import Switch from '@/ui/v2/Switch';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { useController, useFormContext } from 'react-hook-form';
|
||||
import type {
|
||||
ControllerProps,
|
||||
FieldValues,
|
||||
UseControllerProps,
|
||||
} from 'react-hook-form/dist/types';
|
||||
import mergeRefs from 'react-merge-refs';
|
||||
|
||||
export interface ControlledSwitchProps<TFieldValues extends FieldValues = any>
|
||||
extends SwitchProps {
|
||||
/**
|
||||
* Props passed to the react-hook-form controller.
|
||||
*/
|
||||
controllerProps?: ControllerProps;
|
||||
/**
|
||||
* Name of the field.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Control for the input field.
|
||||
*/
|
||||
control?: UseControllerProps<TFieldValues>['control'];
|
||||
}
|
||||
|
||||
function ControlledSwitch(
|
||||
{ controllerProps, name, control, ...props }: ControlledSwitchProps,
|
||||
ref: ForwardedRef<HTMLSpanElement>,
|
||||
) {
|
||||
const { setValue } = useFormContext();
|
||||
const { field } = useController({
|
||||
...controllerProps,
|
||||
name: controllerProps?.name || name || '',
|
||||
control: controllerProps?.control || control,
|
||||
});
|
||||
|
||||
return (
|
||||
<Switch
|
||||
{...props}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(e) => {
|
||||
setValue(controllerProps?.name || name, e.target.checked, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
checked={field.value || false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(ControlledSwitch);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ControlledSwitch';
|
||||
export { default } from './ControlledSwitch';
|
||||
@@ -2,11 +2,11 @@ import DataGridBody from '@/components/common/DataGridBody';
|
||||
import DataGridFrame from '@/components/common/DataGridFrame';
|
||||
import type { DataGridHeaderProps } from '@/components/common/DataGridHeader';
|
||||
import DataGridHeader from '@/components/common/DataGridHeader';
|
||||
import DataBrowserEmptyState from '@/components/data-browser/DataBrowserEmptyState';
|
||||
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
|
||||
import { DataGridProvider } from '@/context/DataGridContext';
|
||||
import type { UseDataGridOptions } from '@/hooks/useDataGrid';
|
||||
import useDataGrid from '@/hooks/useDataGrid';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef, useEffect, useRef } from 'react';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DataGridProps } from '@/components/common/DataGrid';
|
||||
import DataGridCell from '@/components/common/DataGridCell';
|
||||
import useDataGridConfig from '@/hooks/useDataGridConfig';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
|
||||
@@ -35,15 +35,15 @@ function InsertPlaceholderTableRow({
|
||||
}: InsertPlaceholderTableRowProps) {
|
||||
return (
|
||||
<div
|
||||
className="h-12 border-r-1 border-b-1 border-gray-200 bg-white"
|
||||
className="h-12 bg-white border-gray-200 border-r-1 border-b-1"
|
||||
{...props}
|
||||
>
|
||||
<Button
|
||||
onClick={onInsertRow}
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="h-full w-full justify-start rounded-none px-2 py-3 text-xs font-normal hover:shadow-none focus:shadow-none focus:outline-none"
|
||||
startIcon={<PlusIcon className="h-4 w-4 text-greyscaleGrey" />}
|
||||
className="justify-start w-full h-full px-2 py-3 text-xs font-normal rounded-none hover:shadow-none focus:shadow-none focus:outline-none"
|
||||
startIcon={<PlusIcon className="w-4 h-4 text-greyscaleGrey" />}
|
||||
>
|
||||
Insert New Row
|
||||
</Button>
|
||||
@@ -181,7 +181,7 @@ export default function DataGridBody<T extends object>({
|
||||
return (
|
||||
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
||||
{rows.length === 0 && !loading && (
|
||||
<div className="flex flex-nowrap pr-5">
|
||||
<div className="flex pr-5 flex-nowrap">
|
||||
{onInsertRow ? (
|
||||
<InsertPlaceholderTableRow
|
||||
style={{
|
||||
@@ -279,7 +279,7 @@ export default function DataGridBody<T extends object>({
|
||||
})}
|
||||
|
||||
{allowInsertColumn && (
|
||||
<div className="h-12 w-25 border-r-1 border-b-1 border-gray-200 bg-white" />
|
||||
<div className="h-12 bg-white border-gray-200 w-25 border-r-1 border-b-1" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ColumnType,
|
||||
DataBrowserGridCell,
|
||||
DataBrowserGridCellProps,
|
||||
} from '@/types/data-browser';
|
||||
} from '@/types/dataBrowser';
|
||||
import Tooltip, { useTooltip } from '@/ui/v2/Tooltip';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import type {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user