Compare commits
1067 Commits
@nhost/das
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
809a2d35f8 | ||
|
|
e17ec7fce7 | ||
|
|
241175b158 | ||
|
|
51ceaf2696 | ||
|
|
490b77cde4 | ||
|
|
7fea29a8b4 | ||
|
|
1a34e011ad | ||
|
|
395839f449 | ||
|
|
399009d66a | ||
|
|
12eb236c4a | ||
|
|
2218e5cd5b | ||
|
|
2dcf1b38c6 | ||
|
|
ad49c92879 | ||
|
|
13dd57eeb4 | ||
|
|
1345741b11 | ||
|
|
511615f176 | ||
|
|
1198c201f1 | ||
|
|
f9b81a2ae9 | ||
|
|
dff0894f37 | ||
|
|
80f3645d57 | ||
|
|
7ddb9a654e | ||
|
|
71f3be15d8 | ||
|
|
96a9070836 | ||
|
|
329e5a91b9 | ||
|
|
6d559d6e23 | ||
|
|
f4f1450d06 | ||
|
|
a1eea9df7d | ||
|
|
26f2b665e6 | ||
|
|
224a5cc805 | ||
|
|
59125b3c77 | ||
|
|
5b69e3efd8 | ||
|
|
c9a444d048 | ||
|
|
2eeac45718 | ||
|
|
fe8ca8aba6 | ||
|
|
086ee46b08 | ||
|
|
203bc97f51 | ||
|
|
b24af44aac | ||
|
|
cdaa6d4e73 | ||
|
|
09e2c8f5c7 | ||
|
|
4581677830 | ||
|
|
7bfa6c9f93 | ||
|
|
80ef430d70 | ||
|
|
bad8af0fd1 | ||
|
|
69caa34c43 | ||
|
|
1d898e2893 | ||
|
|
e87621cbde | ||
|
|
0d6fc42158 | ||
|
|
f6fbee6b13 | ||
|
|
1230b72222 | ||
|
|
6cc7704555 | ||
|
|
c0954dec09 | ||
|
|
6c25480a7a | ||
|
|
da03bf390c | ||
|
|
3b513be9f2 | ||
|
|
e450e9d636 | ||
|
|
ed1ee10879 | ||
|
|
a6120bf366 | ||
|
|
349aac369e | ||
|
|
5a84362c80 | ||
|
|
b23dc058a6 | ||
|
|
ad0dda7493 | ||
|
|
4063507d59 | ||
|
|
f59a77b1c8 | ||
|
|
30686bc4ce | ||
|
|
0c4ac8d368 | ||
|
|
85439307a9 | ||
|
|
0d8baa4065 | ||
|
|
7da0e5e256 | ||
|
|
4bca94425e | ||
|
|
9f948385c0 | ||
|
|
294c504b61 | ||
|
|
1469ec2969 | ||
|
|
8229101efe | ||
|
|
afad1778f8 | ||
|
|
28fc7b84c7 | ||
|
|
3f478a4e3c | ||
|
|
aa54666941 | ||
|
|
20fb69faba | ||
|
|
1caeb2a548 | ||
|
|
6356c5a2c8 | ||
|
|
6ec1dd3248 | ||
|
|
8a4b5031dc | ||
|
|
4790fee41f | ||
|
|
0a8033812d | ||
|
|
2b56ffc29e | ||
|
|
aa9b926cd7 | ||
|
|
575404ad62 | ||
|
|
3f6dfc7bcd | ||
|
|
682e64d7a3 | ||
|
|
30cee4f86c | ||
|
|
29dcc8c63e | ||
|
|
d926f15676 | ||
|
|
726c33d1b2 | ||
|
|
11b9cfbc0d | ||
|
|
d4a0aad2dd | ||
|
|
1030813279 | ||
|
|
917a14aa40 | ||
|
|
6381d1b095 | ||
|
|
8dbdc0bf50 | ||
|
|
8c072a4c6e | ||
|
|
fe341519f7 | ||
|
|
ea09384064 | ||
|
|
49b9972885 | ||
|
|
98c541ee52 | ||
|
|
79aaa91e67 | ||
|
|
df4d24320a | ||
|
|
757c888656 | ||
|
|
7c13eb5f9b | ||
|
|
a84608e086 | ||
|
|
e43c079b9c | ||
|
|
3f396a9ebb | ||
|
|
6ed605beb8 | ||
|
|
edd223d29c | ||
|
|
baa3ef794e | ||
|
|
da7ffbe523 | ||
|
|
15a985e079 | ||
|
|
8ff00a4258 | ||
|
|
7e27d7c0a1 | ||
|
|
49f9b8372a | ||
|
|
3ca70554c8 | ||
|
|
077b200510 | ||
|
|
925bf0f13f | ||
|
|
30d35f9607 | ||
|
|
755aa56f12 | ||
|
|
4c7e7c57a9 | ||
|
|
36708e2853 | ||
|
|
90c6031189 | ||
|
|
f044dbdb10 | ||
|
|
c2f3bce5f9 | ||
|
|
22d9877b97 | ||
|
|
628e96dcc3 | ||
|
|
3e9d3c42b6 | ||
|
|
a1e7b87c38 | ||
|
|
1bd800359e | ||
|
|
54a204a34e | ||
|
|
b17e8d6f3c | ||
|
|
12e2855f01 | ||
|
|
c1080d9e63 | ||
|
|
e4972b8307 | ||
|
|
2e7ec0697e | ||
|
|
2d9baec9d4 | ||
|
|
7a7750be0b | ||
|
|
0f34f0c6b9 | ||
|
|
d05253183a | ||
|
|
65df016bbc | ||
|
|
3e6ee1ae97 | ||
|
|
6042ed101f | ||
|
|
384bce59bf | ||
|
|
8da291ad4d | ||
|
|
f94eb3c467 | ||
|
|
9baf3f4ac7 | ||
|
|
9c406548e3 | ||
|
|
1c08cd1949 | ||
|
|
adc828a582 | ||
|
|
2f220db84a | ||
|
|
f1ec6b9a93 | ||
|
|
233b7e383e | ||
|
|
7ea469a1e3 | ||
|
|
ebd218c180 | ||
|
|
5ab1626f73 | ||
|
|
444c3b86ca | ||
|
|
7238412341 | ||
|
|
f6639ae05c | ||
|
|
d8ceccec5d | ||
|
|
6db257d4c7 | ||
|
|
93dab2d183 | ||
|
|
dfc18368be | ||
|
|
f7c6e80bf2 | ||
|
|
573cac1431 | ||
|
|
d72ae3f362 | ||
|
|
49ec7ec385 | ||
|
|
7d2b4083c2 | ||
|
|
696b493745 | ||
|
|
15a117a861 | ||
|
|
e7ff1f79f8 | ||
|
|
33c7368a2e | ||
|
|
664c182c8e | ||
|
|
c1ab4e0a77 | ||
|
|
4a4bd61757 | ||
|
|
b6d05289be | ||
|
|
5857458ca5 | ||
|
|
2fb1145fe0 | ||
|
|
546d710102 | ||
|
|
7756103476 | ||
|
|
fef9456c12 | ||
|
|
2d6d56f6b0 | ||
|
|
f54be0fefd | ||
|
|
4e76d388ab | ||
|
|
84b84ab785 | ||
|
|
ed66769688 | ||
|
|
899732f280 | ||
|
|
037b566e39 | ||
|
|
829f20c83c | ||
|
|
f1b5a944a3 | ||
|
|
5ccb764ae5 | ||
|
|
ef2b639734 | ||
|
|
a5b895a827 | ||
|
|
b441b4bae2 | ||
|
|
a6c67c1e4c | ||
|
|
7f1785ac0f | ||
|
|
a0298e0bdb | ||
|
|
3fd94b1cdf | ||
|
|
61d5f7d616 | ||
|
|
cde9a0a715 | ||
|
|
eae6349b04 | ||
|
|
211b930b84 | ||
|
|
4ae463074b | ||
|
|
1c5a4746f7 | ||
|
|
d6ae1fa44a | ||
|
|
a3abb81b37 | ||
|
|
ec74e7fe98 | ||
|
|
6713c198c6 | ||
|
|
35a6b9cf47 | ||
|
|
79f97fad76 | ||
|
|
2faf79077d | ||
|
|
4972b6feb6 | ||
|
|
23d5861c4c | ||
|
|
098ac5a71c | ||
|
|
3a15329cfd | ||
|
|
c3e798aa1d | ||
|
|
eec5e6a93d | ||
|
|
d964b689cd | ||
|
|
1e080c1af5 | ||
|
|
177bba7ec0 | ||
|
|
a593b45dc2 | ||
|
|
b384fb8bd8 | ||
|
|
abd8620ded | ||
|
|
e62ccdcaae | ||
|
|
46d01b09d6 | ||
|
|
ff74e712f8 | ||
|
|
770794ccad | ||
|
|
aa80d1795d | ||
|
|
eaa7720c65 | ||
|
|
7f447d1182 | ||
|
|
5d3dd84762 | ||
|
|
c625317342 | ||
|
|
117398f5dc | ||
|
|
4e421eb4bd | ||
|
|
771447b089 | ||
|
|
8ab75a4146 | ||
|
|
607f465616 | ||
|
|
668c877130 | ||
|
|
4bd870eb96 | ||
|
|
39b3161d91 | ||
|
|
ae090a6585 | ||
|
|
be4831ae62 | ||
|
|
4fb0c18c32 | ||
|
|
22cdd7f8d7 | ||
|
|
f3a91a1f76 | ||
|
|
1e9b92fcf8 | ||
|
|
6cc56066c2 | ||
|
|
99e80cea44 | ||
|
|
f2f1c01e3b | ||
|
|
2c0f98e85c | ||
|
|
a3ad84925c | ||
|
|
b8611b6a1c | ||
|
|
a0e3030005 | ||
|
|
0cf1f1d938 | ||
|
|
88f026066f | ||
|
|
185bef878d | ||
|
|
a1c7b00e74 | ||
|
|
6da4562e79 | ||
|
|
e44cfcb2f2 | ||
|
|
23fabaf8a6 | ||
|
|
f4dca9836f | ||
|
|
f2704ea149 | ||
|
|
dd1b053212 | ||
|
|
d4ccc65655 | ||
|
|
2c2570fc82 | ||
|
|
a60f26966b | ||
|
|
a988de2d61 | ||
|
|
de54ca460e | ||
|
|
afdffab743 | ||
|
|
4c61520397 | ||
|
|
f02cd444d5 | ||
|
|
7f45a51aca | ||
|
|
08e70b9df9 | ||
|
|
32f92489a4 | ||
|
|
20a83362ee | ||
|
|
20b800c3e4 | ||
|
|
94c9cd151a | ||
|
|
0e9eb18052 | ||
|
|
bfaa5b4c4a | ||
|
|
52ec6fe70c | ||
|
|
43b1b1442c | ||
|
|
b06239cc14 | ||
|
|
73dde87a65 | ||
|
|
7e7d810b74 | ||
|
|
b6b2403562 | ||
|
|
9a1f095a45 | ||
|
|
a1a00b33ad | ||
|
|
a3b1ffe77c | ||
|
|
4f22ab3a99 | ||
|
|
a269f4ca3f | ||
|
|
411cb65ba4 | ||
|
|
f691c1f753 | ||
|
|
b299cfc943 | ||
|
|
6157680963 | ||
|
|
1d4bdfa88b | ||
|
|
2755fc43b9 | ||
|
|
0c80d141aa | ||
|
|
f285883c88 | ||
|
|
39f9a325d3 | ||
|
|
e8f66e346f | ||
|
|
98c0535fc9 | ||
|
|
7a61c2e976 | ||
|
|
a15a4db210 | ||
|
|
11fcb8c72f | ||
|
|
a8a20cf5e2 | ||
|
|
2f84bf3251 | ||
|
|
368e0371e9 | ||
|
|
adb5209133 | ||
|
|
63bf405cdd | ||
|
|
d613d66a0a | ||
|
|
e7cb5070cd | ||
|
|
ee50051802 | ||
|
|
20e7bb4747 | ||
|
|
ba0d57ee91 | ||
|
|
98093c9023 | ||
|
|
2fda299736 | ||
|
|
3328ed059e | ||
|
|
cfb7199b79 | ||
|
|
1ad4bfe815 | ||
|
|
25859fc421 | ||
|
|
5a9e7a43c8 | ||
|
|
2739ff90c4 | ||
|
|
93910f27e1 | ||
|
|
04e2d19dda | ||
|
|
a2175f6df7 | ||
|
|
18d415a8fd | ||
|
|
2a4623c582 | ||
|
|
19b7835d92 | ||
|
|
efbd272298 | ||
|
|
98546d24e1 | ||
|
|
fe2c0cf81f | ||
|
|
b28a04c48e | ||
|
|
a014913523 | ||
|
|
706c9dc3fb | ||
|
|
fe08faad4a | ||
|
|
6719ce92ea | ||
|
|
52c6f09bdd | ||
|
|
f337a19875 | ||
|
|
d62c909901 | ||
|
|
99f8f6b370 | ||
|
|
644d94a175 | ||
|
|
05ab111aa4 | ||
|
|
64cf0acd4a | ||
|
|
3d5d530555 | ||
|
|
5e0920ba7c | ||
|
|
9bf6c3b8c4 | ||
|
|
b25a223d90 | ||
|
|
748aa443f4 | ||
|
|
684123e5d6 | ||
|
|
fa045eed15 | ||
|
|
61c0583b6d | ||
|
|
1343a6f252 | ||
|
|
0d73e87a83 | ||
|
|
1ee0d332bf | ||
|
|
130ce49c76 | ||
|
|
6be6d6475a | ||
|
|
177b146b93 | ||
|
|
3cb673000a | ||
|
|
09cf5d4b39 | ||
|
|
48c0061a0b | ||
|
|
0795d1c6a1 | ||
|
|
45a23dd1bf | ||
|
|
bb8e3454df | ||
|
|
6a290bb297 | ||
|
|
80baec7356 | ||
|
|
feb195fd65 | ||
|
|
baaa510309 | ||
|
|
a84aa5ad68 | ||
|
|
8e43297564 | ||
|
|
bb8eb9e387 | ||
|
|
5b0dc6cb19 | ||
|
|
826112afd0 | ||
|
|
97105c390d | ||
|
|
8e3707ff2c | ||
|
|
7453bf3b6a | ||
|
|
bd739383d2 | ||
|
|
f75e2e41db | ||
|
|
7328491be0 | ||
|
|
11b4d12f12 | ||
|
|
6c24d56b1d | ||
|
|
0a523f4b45 | ||
|
|
12301e6551 | ||
|
|
8440d0389e | ||
|
|
c166dad0f8 | ||
|
|
e31d39b3d2 | ||
|
|
090f9cef86 | ||
|
|
74e52cac2d | ||
|
|
f17823760a | ||
|
|
bb8803a1e3 | ||
|
|
b846291331 | ||
|
|
2b2fb94f00 | ||
|
|
551760c4f0 | ||
|
|
5ae5a8e77d | ||
|
|
56aae0c964 | ||
|
|
a0e093d77b | ||
|
|
5e82e1b3da | ||
|
|
e618b705e7 | ||
|
|
a232c9f0f6 | ||
|
|
bf4644ea10 | ||
|
|
0aca907ea4 | ||
|
|
394f4c4174 | ||
|
|
8fef08a150 | ||
|
|
1bd2c37301 | ||
|
|
5cdb70bd81 | ||
|
|
1a5f80e1b6 | ||
|
|
59e0cb00c5 | ||
|
|
406b0f2cb7 | ||
|
|
d329b6218f | ||
|
|
335b58670e | ||
|
|
efa2d89067 | ||
|
|
77ce4bd738 | ||
|
|
017adea700 | ||
|
|
378284faa8 | ||
|
|
e5e2d114b1 | ||
|
|
5e3dbdeb7d | ||
|
|
98b777491a | ||
|
|
71de870cb0 | ||
|
|
74d4deba28 | ||
|
|
cb248f0d30 | ||
|
|
09e4f1eb34 | ||
|
|
19818e2b59 | ||
|
|
6e1f03eaee | ||
|
|
b3eeec82ef | ||
|
|
34ff254696 | ||
|
|
867c807699 | ||
|
|
1c4806bf51 | ||
|
|
2fb82ec97d | ||
|
|
d0673d7825 | ||
|
|
106f23dcfa | ||
|
|
0c994a9651 | ||
|
|
83ef755822 | ||
|
|
b7703ffd70 | ||
|
|
4713cecfc2 | ||
|
|
340ea5b115 | ||
|
|
f79eebadbf | ||
|
|
ac174b5e51 | ||
|
|
dc9ddfc9ae | ||
|
|
3bdd9f570c | ||
|
|
94477be998 | ||
|
|
568577e8ca | ||
|
|
e93b06ab8f | ||
|
|
c75bf46ba1 | ||
|
|
63a1fd09b5 | ||
|
|
630d44ad6e | ||
|
|
d7db521974 | ||
|
|
90e4053f0a | ||
|
|
4191b933c9 | ||
|
|
8e9d5d1b38 | ||
|
|
43c86fef14 | ||
|
|
6b97340cf4 | ||
|
|
1605756362 | ||
|
|
6437544384 | ||
|
|
776eca3fb5 | ||
|
|
b4dcd1996d | ||
|
|
7fb73dbb1b | ||
|
|
a66b11d245 | ||
|
|
912ed76c64 | ||
|
|
b47c0d1af7 | ||
|
|
b97ab2be2f | ||
|
|
f12cb666ff | ||
|
|
c3b2b1cd02 | ||
|
|
c0b71102d4 | ||
|
|
5f68ae95c4 | ||
|
|
2d1b7bb292 | ||
|
|
ee154d4eca | ||
|
|
58ef9bbe02 | ||
|
|
f3f35beefd | ||
|
|
d31330e6c0 | ||
|
|
c3dda79d95 | ||
|
|
7c1273725d | ||
|
|
70be0e1ab4 | ||
|
|
4f5870cfd7 | ||
|
|
623607476e | ||
|
|
1e232713d9 | ||
|
|
1ed647c4e9 | ||
|
|
b666a173b1 | ||
|
|
caba147b32 | ||
|
|
ca365fc8e7 | ||
|
|
d88cdedb26 | ||
|
|
1de08cecaf | ||
|
|
47bb997036 | ||
|
|
4e4d962f30 | ||
|
|
883fb82c77 | ||
|
|
c9f5634ac2 | ||
|
|
6ee9a589fb | ||
|
|
e2d733cf34 | ||
|
|
a0d7327c8d | ||
|
|
c7c8a20334 | ||
|
|
cca8de5805 | ||
|
|
8c065c42d6 | ||
|
|
210af3a3e8 | ||
|
|
fbb12a8079 | ||
|
|
77692ac40e | ||
|
|
2c2a42a8e8 | ||
|
|
a8466798a3 | ||
|
|
a45c0970bb | ||
|
|
9bf30a1ccc | ||
|
|
99d3d82c72 | ||
|
|
43acb3fb50 | ||
|
|
ba9ef13ba3 | ||
|
|
cea507a271 | ||
|
|
9130ab1230 | ||
|
|
27acdd6f56 | ||
|
|
dcdacd73ec | ||
|
|
9c9966a30f | ||
|
|
5a23e7a0a8 | ||
|
|
47500fac39 | ||
|
|
cf2264ce1d | ||
|
|
cbbf53c05b | ||
|
|
11bd011860 | ||
|
|
e3c0c47777 | ||
|
|
d825404b54 | ||
|
|
02dd9dd8c0 | ||
|
|
d46d77ee71 | ||
|
|
a292482705 | ||
|
|
8a4ca41172 | ||
|
|
fd3ce98600 | ||
|
|
04f36a0491 | ||
|
|
5e2ecb4d1e | ||
|
|
eca9e551e8 | ||
|
|
52ebbef762 | ||
|
|
82faa4ca0a | ||
|
|
d06a21764a | ||
|
|
8b54d290a5 | ||
|
|
4cfa6bbe1e | ||
|
|
614f213e26 | ||
|
|
4eebf51821 | ||
|
|
9a52298aa7 | ||
|
|
099eebe602 | ||
|
|
7cce8652e7 | ||
|
|
f2e2323801 | ||
|
|
4e16de6db2 | ||
|
|
798e591b1d | ||
|
|
b48bc034ca | ||
|
|
f57819230b | ||
|
|
3d8067ff7b | ||
|
|
0fa4b428a9 | ||
|
|
8c5864340e | ||
|
|
c131100af9 | ||
|
|
e363fef8cf | ||
|
|
d8072101c8 | ||
|
|
afbba531a1 | ||
|
|
4b6df8b9d6 | ||
|
|
a2af5a674d | ||
|
|
c33c1fd6b9 | ||
|
|
041d9b98e3 | ||
|
|
e4b4940397 | ||
|
|
be91f4ed2a | ||
|
|
ec6ba846cf | ||
|
|
a9e9fc4305 | ||
|
|
d4ff25df0f | ||
|
|
d8d8394b3b | ||
|
|
f051a121b2 | ||
|
|
c547b490e5 | ||
|
|
4f4449b855 | ||
|
|
6ed46ce2d4 | ||
|
|
bfb4c1a6cc | ||
|
|
776c8f9237 | ||
|
|
c0773d82e9 | ||
|
|
c46b1383f2 | ||
|
|
beed2eba21 | ||
|
|
70f9610041 | ||
|
|
e91de1088d | ||
|
|
ce1ee40dab | ||
|
|
bd7929f5ed | ||
|
|
2c8559a319 | ||
|
|
bd5ea5ee3a | ||
|
|
3538dbac39 | ||
|
|
03b5cda69a | ||
|
|
4329d04854 | ||
|
|
ca50c5ce0c | ||
|
|
3d74374780 | ||
|
|
7063af678c | ||
|
|
a3271ed014 | ||
|
|
d4fc99a77c | ||
|
|
d90fcf3c24 | ||
|
|
ee70b226fc | ||
|
|
2b44a1cf27 | ||
|
|
c4f60b3645 | ||
|
|
f86f658aa5 | ||
|
|
bd02bd3f3e | ||
|
|
a133faa797 | ||
|
|
227ef968e6 | ||
|
|
430b37b2e1 | ||
|
|
bb0269691d | ||
|
|
124620c33e | ||
|
|
ce3ece1ad7 | ||
|
|
c81002622c | ||
|
|
35fa6bb043 | ||
|
|
8d6171d22d | ||
|
|
fff178d79f | ||
|
|
5e5e454ae7 | ||
|
|
ce005f6d9e | ||
|
|
a4469a5942 | ||
|
|
b8f11a13d7 | ||
|
|
1d1555593f | ||
|
|
001b3dccec | ||
|
|
6755dfb17b | ||
|
|
2ac90dfdec | ||
|
|
093f3906a4 | ||
|
|
6fb81a27ba | ||
|
|
9be41bf594 | ||
|
|
cbb1fc5bc8 | ||
|
|
85889ee882 | ||
|
|
99fcc36250 | ||
|
|
351873059e | ||
|
|
8ccfc10522 | ||
|
|
82b02ca70b | ||
|
|
14fc132040 | ||
|
|
a35da349ed | ||
|
|
302e1d9d33 | ||
|
|
0db40184e8 | ||
|
|
d38649494e | ||
|
|
5f22f1b5e5 | ||
|
|
494f93a4bf | ||
|
|
7e4a756cfe | ||
|
|
5bf61583e0 | ||
|
|
7eac17a1cb | ||
|
|
84c8af232c | ||
|
|
7f101d54da | ||
|
|
75b497412e | ||
|
|
5bd774afbb | ||
|
|
cdfe203fe4 | ||
|
|
a41aeeb9ef | ||
|
|
4c7d32e944 | ||
|
|
447c622fc0 | ||
|
|
03f22aed72 | ||
|
|
e33df513ff | ||
|
|
ede5abf2ac | ||
|
|
0bdd1d0e0c | ||
|
|
61de7b21fd | ||
|
|
323fd5cbe3 | ||
|
|
4b6ead1b17 | ||
|
|
0b193e6310 | ||
|
|
b21a5403fe | ||
|
|
0ec3abf47c | ||
|
|
ffb3c426d3 | ||
|
|
889ee6589e | ||
|
|
ae19105302 | ||
|
|
730a482598 | ||
|
|
e8320be941 | ||
|
|
b00d261916 | ||
|
|
6e05ab4628 | ||
|
|
5223ee9353 | ||
|
|
c8c5ace7cc | ||
|
|
c6a4c28579 | ||
|
|
850a049ca2 | ||
|
|
eff3f0aefd | ||
|
|
2b1338f716 | ||
|
|
2b58c60747 | ||
|
|
1b2e3fbd1d | ||
|
|
6f4fdcf73f | ||
|
|
cb529dc60c | ||
|
|
68a449dbfc | ||
|
|
7d0c6d083a | ||
|
|
1353477da1 | ||
|
|
549c7cb7eb | ||
|
|
e131c12d5d | ||
|
|
8bb097c9a7 | ||
|
|
ea31e64a71 | ||
|
|
369b931689 | ||
|
|
e1ec5c1be2 | ||
|
|
9822a160d4 | ||
|
|
7c67a2c437 | ||
|
|
8e8884f4e1 | ||
|
|
9923be41ce | ||
|
|
3141ce5b68 | ||
|
|
9c22a616a7 | ||
|
|
6bc67e95a5 | ||
|
|
0f6074c16f | ||
|
|
c96d7ccdf2 | ||
|
|
fde7ac7c1c | ||
|
|
24ef6071cc | ||
|
|
bb993b6b03 | ||
|
|
89ca34be9a | ||
|
|
b66d095c95 | ||
|
|
0bad9ff4fa | ||
|
|
9a761f4fec | ||
|
|
253dd235ca | ||
|
|
991e8f2d15 | ||
|
|
e500e87022 | ||
|
|
bd6b55868a | ||
|
|
afb3fe490e | ||
|
|
c684d0307b | ||
|
|
eaebd2b028 | ||
|
|
f03ecd91a9 | ||
|
|
96f17c39b1 | ||
|
|
2d657b9c29 | ||
|
|
cb7c8c6398 | ||
|
|
4bf40995b5 | ||
|
|
ab5f704280 | ||
|
|
f65e4de955 | ||
|
|
7e0e4d05aa | ||
|
|
decb0b057c | ||
|
|
0954a44f84 | ||
|
|
700cbd9e47 | ||
|
|
3238543b08 | ||
|
|
fc79b890df | ||
|
|
211eb42af5 | ||
|
|
a7398451e3 | ||
|
|
4b4f0d0150 | ||
|
|
f37e2a23e2 | ||
|
|
3f8d68ffab | ||
|
|
f7e706724c | ||
|
|
f46d96bafc | ||
|
|
6b8acd35bd | ||
|
|
2832d7299f | ||
|
|
44c5b386c3 | ||
|
|
44ff6a059f | ||
|
|
1a4a061284 | ||
|
|
5a91c477f0 | ||
|
|
66f73d06a8 | ||
|
|
35d52aab87 | ||
|
|
8261743bd3 | ||
|
|
ddd41aae99 | ||
|
|
78555c7e85 | ||
|
|
01ded8ffff | ||
|
|
3c7cf92edf | ||
|
|
bb4301fd34 | ||
|
|
34cf1d79a0 | ||
|
|
9d4542b3db | ||
|
|
bb5dbdf5a3 | ||
|
|
2801b03bf4 | ||
|
|
8298d458d5 | ||
|
|
6e9b941b89 | ||
|
|
5dd25941e5 | ||
|
|
832210d8ad | ||
|
|
a09dad060c | ||
|
|
76b63debf0 | ||
|
|
e88684ff2a | ||
|
|
c8c8948755 | ||
|
|
17e9e5899e | ||
|
|
bd22c48131 | ||
|
|
095d6e918c | ||
|
|
89a239ff3a | ||
|
|
0131886011 | ||
|
|
340c014fe8 | ||
|
|
bc9c8b9456 | ||
|
|
c22b2621ba | ||
|
|
726746c4d3 | ||
|
|
c431570783 | ||
|
|
445d8ef449 | ||
|
|
0f4ea18e42 | ||
|
|
dae7c5d517 | ||
|
|
f673adea00 | ||
|
|
1c6f1e3b33 | ||
|
|
6593e8d3eb | ||
|
|
d1365ea516 | ||
|
|
72dbba7881 | ||
|
|
a3f3991d5a | ||
|
|
c71fe2cf72 | ||
|
|
24c5ed3ea4 | ||
|
|
2d9145f918 | ||
|
|
9a0ab5b887 | ||
|
|
1ddf704c5b | ||
|
|
6f4ee845c6 | ||
|
|
0368663dea | ||
|
|
76ce7d7b6e | ||
|
|
538bfbcb3e | ||
|
|
07b35d1e5f | ||
|
|
2200a0ed07 | ||
|
|
9219838127 | ||
|
|
df23d97126 | ||
|
|
43b68a79eb | ||
|
|
104f149369 | ||
|
|
01228583a0 | ||
|
|
93309dd851 | ||
|
|
2cc18dcb51 | ||
|
|
3b48a62790 | ||
|
|
8897dec056 | ||
|
|
324dda8309 | ||
|
|
ac845c6d92 | ||
|
|
cfcb97b8ee | ||
|
|
95f62bed07 | ||
|
|
892ad66ba1 | ||
|
|
f4af81020b | ||
|
|
0e4d8ff118 | ||
|
|
6999562b59 | ||
|
|
baec5bada7 | ||
|
|
4e56cfc628 | ||
|
|
d167121093 | ||
|
|
822e251b11 | ||
|
|
328c6bb486 | ||
|
|
bef8198cbf | ||
|
|
179313d8a2 | ||
|
|
54bc91923f | ||
|
|
c3ce004f46 | ||
|
|
a1ffad77eb | ||
|
|
77b12feb95 | ||
|
|
32d4670bbb | ||
|
|
1dc09942d2 | ||
|
|
3343a36358 | ||
|
|
de4d59da99 | ||
|
|
b755e9086c | ||
|
|
48866d0ee1 | ||
|
|
7d577a68b7 | ||
|
|
5b3b76bd41 | ||
|
|
7f7e7ea7d4 | ||
|
|
aaaf2dc9c5 | ||
|
|
fa9c1ea28c | ||
|
|
87eda76e2b | ||
|
|
982059e18e | ||
|
|
02c0586467 | ||
|
|
8a596f2a9e | ||
|
|
89b70eb93c | ||
|
|
d6d2381598 | ||
|
|
860d872d07 | ||
|
|
b9917c0c69 | ||
|
|
bf1e4071db | ||
|
|
e5601581f5 | ||
|
|
5013213bc3 | ||
|
|
8be094be54 | ||
|
|
43e5221119 | ||
|
|
6f8feaffc5 | ||
|
|
284ef7e7f2 | ||
|
|
6d5c202da9 | ||
|
|
962563d6a0 | ||
|
|
8bf58ba26b | ||
|
|
0c175e7a11 | ||
|
|
70f2fbcfc2 | ||
|
|
d2c4ad3260 | ||
|
|
a9ca2c2946 | ||
|
|
d854dd74b1 | ||
|
|
cf347341d4 | ||
|
|
5262fac6d5 | ||
|
|
d6c670a78b | ||
|
|
197e406209 | ||
|
|
6f0ac5706c | ||
|
|
5fc78964dc | ||
|
|
1bb71df7a1 | ||
|
|
5880f0cd17 | ||
|
|
d7404e2247 | ||
|
|
2aa91cf266 | ||
|
|
b2b17d71aa | ||
|
|
0785a41070 | ||
|
|
c6c8569088 | ||
|
|
c38f60740e | ||
|
|
9342937440 | ||
|
|
e89cd4e262 | ||
|
|
a05438352b | ||
|
|
78437959bb | ||
|
|
e1a7433adb | ||
|
|
a37a430b9f | ||
|
|
7b970e6858 | ||
|
|
5bdb2c605d | ||
|
|
a08cfc696d | ||
|
|
9c4223c547 | ||
|
|
7de3ed5323 | ||
|
|
83f4e32972 | ||
|
|
7b86f38ba3 | ||
|
|
0753e6529c | ||
|
|
a689b0dbe0 | ||
|
|
14235d08bc | ||
|
|
e0a987248f | ||
|
|
1303fc60ac | ||
|
|
e87a14a3fe | ||
|
|
172cab87c7 | ||
|
|
a463d5c3bf | ||
|
|
041385f19a | ||
|
|
b45aa420d9 | ||
|
|
1d76de3f60 | ||
|
|
47949ed4af | ||
|
|
6a52a267ff | ||
|
|
f33242f2c4 | ||
|
|
b9013e2f6e | ||
|
|
bbe6fb39c8 | ||
|
|
955e3e1316 | ||
|
|
2dba783ad3 | ||
|
|
e23cf74975 | ||
|
|
a3d01c4fad | ||
|
|
4cdcef9ef5 | ||
|
|
df894ef7e2 | ||
|
|
dbc2fadd87 | ||
|
|
f7dd6a9fc6 | ||
|
|
2949ff0f62 | ||
|
|
4578065807 | ||
|
|
feb0c20eb3 | ||
|
|
9e37ca4cbc | ||
|
|
af57ccce0f | ||
|
|
5f44aefcc6 | ||
|
|
168616df38 | ||
|
|
1527b0a455 | ||
|
|
375e53a3f0 | ||
|
|
96e3ca5a32 | ||
|
|
0e570df9c5 | ||
|
|
1f4c67283e | ||
|
|
fc1c4861a3 | ||
|
|
74feaf6add | ||
|
|
e9c8909c6b | ||
|
|
8cd97206cc | ||
|
|
02197639f2 | ||
|
|
38b594aef9 | ||
|
|
f3a8886cd0 | ||
|
|
96f9278c8f | ||
|
|
8d76cf8d40 | ||
|
|
3e1fb974e4 | ||
|
|
9fe2ecd317 | ||
|
|
ada5309b49 | ||
|
|
08698f8246 | ||
|
|
f74871d872 | ||
|
|
0b56e31408 | ||
|
|
d8c45b452d | ||
|
|
c4e3e3f91f | ||
|
|
483fd6c7f4 | ||
|
|
ac37d7bcae | ||
|
|
9adf91ba87 | ||
|
|
d11f6eebb0 | ||
|
|
8a678fbc87 | ||
|
|
6411ec3ec3 | ||
|
|
5187fe76aa | ||
|
|
859f457e4a | ||
|
|
dc2b5b4429 | ||
|
|
b7645e7892 | ||
|
|
3f26056688 | ||
|
|
b1338246aa | ||
|
|
d04ccd600e | ||
|
|
d483ad5602 | ||
|
|
bcf3e6bc2c | ||
|
|
575ff4e9b5 | ||
|
|
2010638540 | ||
|
|
0346495a79 | ||
|
|
2babb0b6f3 | ||
|
|
1f293d0f0c | ||
|
|
af4c886437 | ||
|
|
c182b3ca4b | ||
|
|
d5344ed31f | ||
|
|
adeb2a6d90 | ||
|
|
921243e4d9 | ||
|
|
1c5178f5fb | ||
|
|
72ad9aa8ee | ||
|
|
1b45db8caf | ||
|
|
9ffb4d0295 | ||
|
|
e56340b792 | ||
|
|
814c6d997a | ||
|
|
7d7a352c33 | ||
|
|
53a704fc7d | ||
|
|
c23eddf33d | ||
|
|
d4147f4713 | ||
|
|
f375eaccf5 | ||
|
|
88a992ba36 | ||
|
|
47f79ba9f3 | ||
|
|
2e010455cf | ||
|
|
7e63c822ec | ||
|
|
6a7801be93 | ||
|
|
7bc5bb857c | ||
|
|
c957039d75 | ||
|
|
96c4032424 | ||
|
|
ec70126b56 | ||
|
|
276b7d48c3 | ||
|
|
6925b0d510 | ||
|
|
6ff306c4e4 | ||
|
|
aa440fefe6 | ||
|
|
9fbafc6654 | ||
|
|
86b9f9040c | ||
|
|
b086175045 | ||
|
|
36db12297b | ||
|
|
e5885d9bad | ||
|
|
15c13f3bbe | ||
|
|
8d47cafd86 | ||
|
|
222f03725b | ||
|
|
10b786e5c6 | ||
|
|
aa8ae88d12 | ||
|
|
0f2c86b41a | ||
|
|
408cb6d10c | ||
|
|
4d882703f2 | ||
|
|
437dacaa9e | ||
|
|
088584e79d | ||
|
|
a4c76892dd | ||
|
|
00d278b2cc | ||
|
|
cb6b5faeb9 | ||
|
|
7c4c847b91 | ||
|
|
908887d8c5 | ||
|
|
a2d67bc2db | ||
|
|
1a6cd78254 | ||
|
|
6500629c4b | ||
|
|
add3c2c10e | ||
|
|
dd29b06260 | ||
|
|
490cb25a0f | ||
|
|
0df0dd741e | ||
|
|
2172946879 | ||
|
|
40e50f0e75 | ||
|
|
65cf0888b5 | ||
|
|
21833019ca | ||
|
|
b3171ba3e9 | ||
|
|
6f01f19d02 | ||
|
|
ce92b01eac | ||
|
|
e24a177434 | ||
|
|
56a52b6d48 | ||
|
|
92bfa8c723 | ||
|
|
2a52aaa4a6 | ||
|
|
8280a3e9d8 | ||
|
|
523f60bf68 | ||
|
|
19b11d4084 | ||
|
|
805bae1507 | ||
|
|
f6c014c06f | ||
|
|
c5794f4596 | ||
|
|
fc28817380 | ||
|
|
80bbd3a165 | ||
|
|
7a10617a72 | ||
|
|
f0b6dca1a5 | ||
|
|
5db20adc34 | ||
|
|
12dc41a517 | ||
|
|
768fd56891 | ||
|
|
8a508cb1cc | ||
|
|
34f6a8eef4 | ||
|
|
c9d2d31a9b | ||
|
|
68fb23a361 | ||
|
|
476139e528 | ||
|
|
6a850818a0 | ||
|
|
3970dbba0d | ||
|
|
8ee2166f0d | ||
|
|
e13500a185 | ||
|
|
411f574a51 | ||
|
|
7fc91b992e | ||
|
|
b840012be0 | ||
|
|
645c51a9dc | ||
|
|
0ce6f05539 | ||
|
|
8b1188af53 | ||
|
|
12b01f8dee | ||
|
|
60f4faf409 | ||
|
|
528dff3f1b | ||
|
|
d429fb4a3e | ||
|
|
816c916709 | ||
|
|
b7a2b8b537 | ||
|
|
261d8cf434 | ||
|
|
41f49bde76 | ||
|
|
65f685bdb2 | ||
|
|
f52a7f4aac | ||
|
|
e71b9903d9 | ||
|
|
325fd08aef | ||
|
|
3888704464 | ||
|
|
38e8a10a29 | ||
|
|
d8545eae12 | ||
|
|
3d5bfd87d2 | ||
|
|
e66c5626bd | ||
|
|
a227c6561e | ||
|
|
e885c159df | ||
|
|
09fcb74bef | ||
|
|
a089197197 | ||
|
|
34f843875b | ||
|
|
ca278a8c39 | ||
|
|
75603786e0 | ||
|
|
4e4e699b94 | ||
|
|
da31fa9fba | ||
|
|
95e2afaf47 | ||
|
|
958a56dde9 | ||
|
|
74cb15930e | ||
|
|
aa37a98424 | ||
|
|
11cbdda3a5 | ||
|
|
6d1f4adf10 | ||
|
|
ddbc50c15e | ||
|
|
b2cbf570a3 | ||
|
|
22b8e65031 | ||
|
|
63c94d2036 | ||
|
|
010df48c1e | ||
|
|
fdc11db93d | ||
|
|
cb4749f168 | ||
|
|
46a8fcf471 | ||
|
|
ce4b655c55 | ||
|
|
dc57d31ec9 | ||
|
|
ea29fd6b73 | ||
|
|
d8e4073957 | ||
|
|
3f399a54a3 |
18
.github/CODEOWNERS
vendored
@@ -1,14 +1,14 @@
|
||||
# Documentation
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
/packages @plmercereau @szilarddoro
|
||||
/packages @szilarddoro
|
||||
/packages/docgen @szilarddoro
|
||||
/integrations/stripe-graphql-js @elitan
|
||||
/.github @plmercereau
|
||||
/dashboard/ @szilarddoro @guicurcio
|
||||
/docs/ @guicurcio @elitan
|
||||
/config/ @plmercereau @szilarddoro
|
||||
/examples/ @plmercereau
|
||||
/examples/codegen-react-apollo @elitan @plmercereau
|
||||
/examples/codegen-react-query @elitan @plmercereau
|
||||
/examples/react-apollo-crm @elitan @plmercereau
|
||||
/.github @szilarddoro
|
||||
/dashboard/ @szilarddoro
|
||||
/docs/ @elitan
|
||||
/config/ @szilarddoro
|
||||
/examples/ @szilarddoro
|
||||
/examples/codegen-react-apollo @elitan @szilarddoro
|
||||
/examples/codegen-react-query @elitan @szilarddoro
|
||||
/examples/react-apollo-crm @elitan @szilarddoro
|
||||
|
||||
10
.github/actions/install-dependencies/action.yaml
vendored
@@ -14,7 +14,7 @@ runs:
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.17.0
|
||||
version: 8.4.0
|
||||
run_install: false
|
||||
- name: Get pnpm cache directory
|
||||
id: pnpm-cache-dir
|
||||
@@ -23,9 +23,7 @@ runs:
|
||||
- uses: actions/cache@v3
|
||||
id: pnpm-cache
|
||||
with:
|
||||
path: |
|
||||
${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||
~/.cache/Cypress
|
||||
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: ${{ runner.os }}-node-
|
||||
- name: Use Node.js 16
|
||||
@@ -40,14 +38,14 @@ runs:
|
||||
- shell: bash
|
||||
name: Build packages
|
||||
if: ${{ inputs.BUILD == 'all' }}
|
||||
run: pnpm build:all
|
||||
run: pnpm run 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
|
||||
run: pnpm run build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
|
||||
16
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
daysUntilStale: 180
|
||||
daysUntilClose: 7
|
||||
limitPerRun: 30
|
||||
onlyLabels: []
|
||||
exemptLabels: []
|
||||
|
||||
exemptProjects: false
|
||||
exemptMilestones: false
|
||||
exemptAssignees: false
|
||||
staleLabel: stale
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
4
.github/workflows/changesets.yaml
vendored
@@ -98,7 +98,7 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push to Docker Hub
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
timeout-minutes: 60
|
||||
with:
|
||||
context: .
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
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
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
commit-message: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
|
||||
|
||||
104
.github/workflows/ci.yaml
vendored
@@ -8,7 +8,6 @@ on:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
@@ -20,6 +19,12 @@ env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
|
||||
NHOST_TEST_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_NAME }}
|
||||
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
|
||||
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -56,52 +61,11 @@ jobs:
|
||||
| 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)
|
||||
| jq -c --slurp 'map(select(length > 0))')
|
||||
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
e2e:
|
||||
name: 'e2e (${{ matrix.package.path }})'
|
||||
needs: build
|
||||
if: ${{ needs.build.outputs.matrix != '[]' && needs.build.outputs.matrix != '' }}
|
||||
strategy:
|
||||
# * Don't cancel other matrices when one fails
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.build.outputs.matrix) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# * 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)) != ''
|
||||
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
|
||||
- id: file-name
|
||||
if: ${{ failure() }}
|
||||
name: Tranform package name into a valid file name
|
||||
run: |
|
||||
PACKAGE_FILE_NAME=$(echo "${{ matrix.package.name }}" | sed 's/@//g; s/\//-/g')
|
||||
echo "fileName=$PACKAGE_FILE_NAME" >> $GITHUB_OUTPUT
|
||||
# * Run this step only if the previous step failed, and some Cypress screenshots/videos exist
|
||||
- name: Upload Cypress videos and screenshots
|
||||
if: ${{ failure() && hashFiles(format('{0}/cypress/screenshots/**', matrix.package.path), format('{0}/cypress/videos/**', matrix.package.path)) != ''}}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-${{ steps.file-name.outputs.fileName }}
|
||||
path: |
|
||||
${{format('{0}/cypress/screenshots/**', matrix.package.path)}}
|
||||
${{format('{0}/cypress/videos/**', matrix.package.path)}}
|
||||
|
||||
unit:
|
||||
name: Unit tests
|
||||
needs: build
|
||||
@@ -142,3 +106,57 @@ jobs:
|
||||
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Lint
|
||||
run: pnpm run lint:all
|
||||
|
||||
e2e:
|
||||
name: 'E2E (Package: ${{ matrix.package.path }})'
|
||||
needs: build
|
||||
if: ${{ needs.build.outputs.matrix != '[]' && needs.build.outputs.matrix != '' }}
|
||||
strategy:
|
||||
# * Don't cancel other matrices when one fails
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.build.outputs.matrix) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# * 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)) != ''
|
||||
uses: ./.github/actions/nhost-cli
|
||||
- name: Fetch Dashboard Preview URL
|
||||
id: fetch-dashboard-preview-url
|
||||
uses: zentered/vercel-preview-url@v1.1.9
|
||||
if: github.ref_name != 'main'
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
GITHUB_REF: ${{ github.ref_name }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
with:
|
||||
vercel_team_id: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
vercel_project_id: ${{ secrets.DASHBOARD_STAGING_VERCEL_PROJECT_ID }}
|
||||
vercel_state: BUILDING,READY,INITIALIZING
|
||||
- name: Set Dashboard Preview URL
|
||||
if: steps.fetch-dashboard-preview-url.outputs.preview_url != ''
|
||||
run: echo "NHOST_TEST_DASHBOARD_URL=https://${{ steps.fetch-dashboard-preview-url.outputs.preview_url }}" >> $GITHUB_ENV
|
||||
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
|
||||
- name: Run e2e tests
|
||||
run: pnpm --filter="${{ matrix.package.name }}" run e2e
|
||||
- id: file-name
|
||||
if: ${{ failure() }}
|
||||
name: Transform package name into a valid file name
|
||||
run: |
|
||||
PACKAGE_FILE_NAME=$(echo "${{ matrix.package.name }}" | sed 's/@//g; s/\//-/g')
|
||||
echo "fileName=$PACKAGE_FILE_NAME" >> $GITHUB_OUTPUT
|
||||
# * Run this step only if the previous step failed, and Playwright generated a report
|
||||
- name: Upload Playwright Report
|
||||
if: ${{ failure() && hashFiles(format('{0}/playwright-report/**', matrix.package.path)) != ''}}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-${{ steps.file-name.outputs.fileName }}
|
||||
path: ${{format('{0}/playwright-report/**', matrix.package.path)}}
|
||||
|
||||
1
.github/workflows/dashboard.yaml
vendored
@@ -9,6 +9,7 @@ env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
|
||||
89
.github/workflows/renovate.yaml
vendored
@@ -1,89 +0,0 @@
|
||||
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 }}
|
||||
@@ -23,8 +23,8 @@ module.exports = {
|
||||
'e2e/**/*.ts',
|
||||
'e2e/**/*.d.ts'
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'cypress'],
|
||||
extends: ['plugin:cypress/recommended'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module'
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"@nhost/docgen": [
|
||||
"../packages/docgen/src/index.ts"
|
||||
],
|
||||
"@nhost/graphql-js": [
|
||||
"../packages/graphql-js/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-auth-js": [
|
||||
"../packages/hasura-auth-js/src/index.ts"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import dts from 'vite-plugin-dts'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
@@ -21,7 +20,9 @@ export default defineConfig({
|
||||
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
|
||||
entryRoot: 'src',
|
||||
// Was defaulting to true until version 1.7
|
||||
skipDiagnostics: true
|
||||
skipDiagnostics: true,
|
||||
// Was defaulting to true until version 2.0
|
||||
copyDtsFiles: true
|
||||
})
|
||||
],
|
||||
test: {
|
||||
@@ -35,6 +36,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: 'es2019',
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry,
|
||||
@@ -61,7 +63,6 @@ export default defineConfig({
|
||||
'@apollo/client/utilities': '@apollo/client/utilities',
|
||||
'graphql-ws': 'graphql-ws',
|
||||
xstate: 'xstate',
|
||||
axios: 'axios',
|
||||
'js-cookie': 'Cookies',
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
# General Environment Variables
|
||||
NEXT_PUBLIC_ENV=dev
|
||||
NEXT_PUBLIC_NHOST_HASURA_URL=http://localhost:9695
|
||||
NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL=http://localhost:1337
|
||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
|
||||
# Environment Variables for Self Hosting and Local Development
|
||||
NEXT_PUBLIC_NHOST_AUTH_URL=https://local.auth.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_FUNCTIONS_URL=https://local.functions.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_GRAPHQL_URL=https://local.graphql.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_STORAGE_URL=https://local.storage.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=https://local.hasura.nhost.run
|
||||
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=https://local.hasura.nhost.run/v1/migrations
|
||||
NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.nhost.run
|
||||
|
||||
# Environment Variables when running the Nhost Dashboard against the Nhost Backend
|
||||
NEXT_PUBLIC_STRIPE_PK=<nhost_stripe_public_key>
|
||||
NEXT_PUBLIC_GITHUB_APP_INSTALL_URL=<github_app_install_url>
|
||||
NEXT_PUBLIC_ANALYTICS_WRITE_KEY=<analytics_write_key>
|
||||
|
||||
@@ -8,7 +8,11 @@ module.exports = {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
ignorePatterns: ['**/.eslintrc.js', '**/prettier.config.js'],
|
||||
ignorePatterns: [
|
||||
'**/.eslintrc.js',
|
||||
'**/prettier.config.js',
|
||||
'**/next.config.js',
|
||||
],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
@@ -21,6 +25,8 @@ module.exports = {
|
||||
'error',
|
||||
{ allowArrowFunctions: true, allowFunctions: true },
|
||||
],
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
curly: ['error', 'all'],
|
||||
'no-restricted-exports': 'off',
|
||||
@@ -30,6 +36,7 @@ module.exports = {
|
||||
'error',
|
||||
{ ignoreTypeReferences: true },
|
||||
],
|
||||
'no-console': ['warn', { allow: ['error'] }],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
@@ -75,7 +82,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
group: ['@testing-library/react*'],
|
||||
message: 'Please use @/utils/testUtils instead.',
|
||||
message: 'Please use @/tests/testUtils instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
7
dashboard/.gitignore
vendored
@@ -49,4 +49,9 @@ tailwind.json
|
||||
.idea
|
||||
|
||||
# Do not ignore Logs page
|
||||
!src/**/logs*
|
||||
!src/**/logs*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
storageState.json
|
||||
e2e/.auth/*
|
||||
@@ -51,7 +51,7 @@ export const decorators = [
|
||||
(Story) => (
|
||||
<NhostApolloProvider
|
||||
fetchPolicy="cache-first"
|
||||
graphqlUrl="http://localhost:1337/v1/graphql"
|
||||
graphqlUrl="https://local.graphql.nhost.run/v1"
|
||||
>
|
||||
<Story />
|
||||
</NhostApolloProvider>
|
||||
|
||||
@@ -1,5 +1,444 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.16.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 399009d6: fix(gql): don't enter an infinite loop when fetching remote app data
|
||||
- 329e5a91: fix(deployments): use the same sorting of deployments everywhere
|
||||
- 6d559d6e: chore(settings): add under the hood improvements to the settings page
|
||||
- 12eb236c: chore(deps): bump `prettier-plugin-tailwindcss` to `v0.3.0`
|
||||
- f9b81a2a: chore(deps): bump `turbo` to `v1.9.8`
|
||||
- 1345741b: fix(projects): don't redirect to 404 on project creation
|
||||
- Updated dependencies [7fea29a8]
|
||||
- @nhost/react-apollo@5.0.23
|
||||
- @nhost/nextjs@1.13.25
|
||||
|
||||
## 0.16.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1230b722: fix(projects): don't redirect to 404 on when the project is renamed
|
||||
- @nhost/react-apollo@5.0.22
|
||||
- @nhost/nextjs@1.13.24
|
||||
|
||||
## 0.16.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [da03bf39]
|
||||
- @nhost/react-apollo@5.0.21
|
||||
- @nhost/nextjs@1.13.23
|
||||
|
||||
## 0.16.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 349aac36: fix(settings): use region domain when constructing the postgres connection string
|
||||
|
||||
## 0.16.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 20fb69fa: chore(projects): change the way how API URLs are constructed
|
||||
|
||||
## 0.16.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 49f9b837: chore(docker): bump `pnpm` to `v8.4.0` and `turbo` to `v1.9.3`
|
||||
- 3f478a4e: chore(deps): bump `vitest` to `v0.31.0`, `@types/react` to `v18.2.6` and `@types/react-dom` to `v18.2.4`
|
||||
|
||||
## 0.16.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d926f156: fix(projects): redirect to 404 when an invalid project is opened
|
||||
- 49b99728: fix(projects): disable features for non-owner members of workspaces
|
||||
|
||||
## 0.16.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 12e2855f: chore(deps): bump `jsdom` to v22
|
||||
- e4972b83: feat(metrics): add Grafana page
|
||||
|
||||
## 0.16.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3f396a9e: fix(projects): unpause after upgrading a paused project to pro
|
||||
- 3f396a9e: fix(projects): don't redirect to 404 page after project creation
|
||||
|
||||
## 0.16.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [90c60311]
|
||||
- @nhost/react-apollo@5.0.20
|
||||
- @nhost/nextjs@1.13.22
|
||||
|
||||
## 0.16.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0f34f0c6: fix(projects): disallow downgrading to free plan
|
||||
- 8da291ad: chore(deps): bump `@types/react` to v18.2.0 and `@types/react-dom` to v18.2.1
|
||||
|
||||
## 0.16.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- adc828a5: fix(gql): don't enter an infinite loop when fetching remote app data
|
||||
|
||||
## 0.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 2fb1145f: feat(compute): add support for replicas
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d8ceccec: chore(env): remove deprecated `NHOST_BACKEND_URL` environment variable
|
||||
|
||||
## 0.15.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 84b84ab7: fix(projects): filter projects by workspace
|
||||
|
||||
## 0.15.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2faf7907: chore(deps): bump `graphql-request` to v6
|
||||
- f1b5a944: chore(deps): bump `@vitejs/plugin-react` to v4
|
||||
- 7f1785ac: chore(deps): bump `@types/react` to v18.0.37
|
||||
- @nhost/react-apollo@5.0.19
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 85889ee8: feat(dashboard): add Compute management to the settings
|
||||
|
||||
## 0.14.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 668c8771: chore(dialogs): unify dialog management of payment dialogs
|
||||
|
||||
## 0.14.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d4ccc656: chore: cleanup unused code
|
||||
- @nhost/react-apollo@5.0.18
|
||||
- @nhost/nextjs@1.13.21
|
||||
|
||||
## 0.14.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b299cfc9: chore(deps): bump `vitest` to v0.30.0
|
||||
- 411cb65b: chore(projects): refactor workspace and project hooks
|
||||
- 43b1b144: chore(deps): bump `@types/react` to v18.0.34 and `@types/react-dom` to v18.0.11
|
||||
- Updated dependencies [43b1b144]
|
||||
- @nhost/react-apollo@5.0.17
|
||||
- @nhost/nextjs@1.13.20
|
||||
|
||||
## 0.14.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ba0d57ee: fix(i18n): revert i18n library
|
||||
- 3328ed05: feat(projects): improve overview when there is an error
|
||||
|
||||
## 0.14.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5e0920ba: chore(deps): bump `next-seo` to v6
|
||||
- 706c9dc3: chore(deps): bump `@types/react` to 18.0.33
|
||||
- 99f8f6b3: feat(metrics): show metrics on the overview
|
||||
|
||||
## 0.14.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.16
|
||||
|
||||
## 0.14.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3cb67300: fix(logs): don't break UI when clearing time picker
|
||||
- 7453bf3b: feat(projects): show project creator info
|
||||
- c166dad0: chore(tests): improve auth page tests
|
||||
- 6a290bb2: chore(deps): bump `@types/react` to 18.0.32
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.15
|
||||
- @nhost/nextjs@1.13.19
|
||||
|
||||
## 0.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 6e1f03ea: feat(dashboard): add support for the Azure AD provider
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1bd2c373: chore(deps): bump `turbo` to 1.8.6
|
||||
- d329b621: chore(deps): bump `@types/react` to 18.0.30
|
||||
- cb248f0d: fix(tests): avoid name collision in database tests
|
||||
- 867c8076: chore(deps): bump `@types/react` to 18.0.29
|
||||
|
||||
## 0.13.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e93b06ab: fix(dashboard): remove left margin from workspace list on mobile
|
||||
- 1c4806bf: chore(deps): bump `sharp` to 0.32.0
|
||||
- @nhost/react-apollo@5.0.14
|
||||
- @nhost/nextjs@1.13.18
|
||||
|
||||
## 0.13.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 912ed76c: chore(dashboard): bump `@apollo/client` to 3.7.10
|
||||
- Updated dependencies [912ed76c]
|
||||
- @nhost/react-apollo@5.0.13
|
||||
|
||||
## 0.13.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7c127372: chore(dashboard): bump `react-error-boundary` to v4
|
||||
|
||||
## 0.13.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9130ab12: chore(dashboard): bump `yup` to v1 and `@hookform/resolvers` to v3
|
||||
|
||||
## 0.13.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 253dd235: using new mutation to create projects + refactor Create Project page.
|
||||
|
||||
## 0.13.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.12
|
||||
- @nhost/nextjs@1.13.17
|
||||
|
||||
## 0.13.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b48bc034: fix(dashboard): disable new users
|
||||
- 798e591b: fix(dashboard): show correct date in data grid
|
||||
|
||||
## 0.13.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bfb4c1a6: chore(dashboard): remove `useAxios` property
|
||||
- d8d8394b: Dashboard: allow to override hasura admin secret in docker
|
||||
- Updated dependencies [ce1ee40d]
|
||||
- @nhost/nextjs@1.13.16
|
||||
- @nhost/react-apollo@5.0.11
|
||||
|
||||
## 0.13.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- beed2eba: Fix docker entrypoint for dashboard
|
||||
- 2c8559a3: fix(dashboard): refresh project list after deleting a project
|
||||
- 4329d048: chore(dashboard): bump `graphiql` dependencies
|
||||
|
||||
## 0.13.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cbb1fc5b: chore(dashboard): cleanup GraphQL operations
|
||||
|
||||
## 0.13.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 088584e7: feat(dashboard): add support for custom local subdomains
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2ac90dfd: fix(dashboard): improve mobile responsive layout
|
||||
- Updated dependencies [f375eacc]
|
||||
- @nhost/nextjs@1.13.15
|
||||
- @nhost/react-apollo@5.0.10
|
||||
|
||||
## 0.12.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.9
|
||||
- @nhost/nextjs@1.13.14
|
||||
|
||||
## 0.12.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2b1338f7: chore(dashboard): bump `turbo` to 1.8.3
|
||||
- 5223ee93: fix(dashboard): show correct deployment status on the main page
|
||||
- 850a049c: chore(deps): update docker/build-push-action action to v4
|
||||
- Updated dependencies [850a049c]
|
||||
- @nhost/nextjs@1.13.13
|
||||
- @nhost/react-apollo@5.0.8
|
||||
|
||||
## 0.12.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4bf40995: chore(deps): bump `typescript` to `4.9.5`
|
||||
- 8bb097c9: chore(deps): bump `vitest`
|
||||
- 35d52aab: chore(deps): replace `cross-fetch` with `isomorphic-unfetch`
|
||||
- Updated dependencies [4bf40995]
|
||||
- Updated dependencies [8bb097c9]
|
||||
- Updated dependencies [35d52aab]
|
||||
- @nhost/react-apollo@5.0.7
|
||||
- @nhost/nextjs@1.13.12
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c96d7ccd: fix(dashboard): fix docker builds
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d1671210: feat(dashboard): use mimir to manage project configuration
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f65e4de9: chore(deps): bump @graphql-codegen monorepo to v3
|
||||
|
||||
## 0.11.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4b4f0d01: chore(dashboard): improve dialog management
|
||||
|
||||
## 0.11.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.6
|
||||
- @nhost/nextjs@1.13.11
|
||||
|
||||
## 0.11.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 01318860: fix(nhost-js): use correct URL for functions requests
|
||||
- Updated dependencies [01318860]
|
||||
- @nhost/react-apollo@5.0.5
|
||||
- @nhost/nextjs@1.13.10
|
||||
|
||||
## 0.11.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f673adea: fix(dashboard): set correct Content-Type for user creation
|
||||
- 445d8ef4: chore(deps): bump `@nhost/react-apollo` to 5.0.4
|
||||
- 445d8ef4: chore(deps): bump `@nhost/nextjs` to 1.13.9
|
||||
- 0368663d: fix(dashboard): allow permission editing for auth and storage schemas
|
||||
- Updated dependencies [445d8ef4]
|
||||
- Updated dependencies [445d8ef4]
|
||||
- @nhost/react-apollo@5.0.4
|
||||
- @nhost/nextjs@1.13.9
|
||||
|
||||
## 0.11.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b755e908: fix(dashboard): use correct date for last seen
|
||||
- 2d9145f9: chore(deps): revert GraphQL client
|
||||
- 1ddf704c: fix(dashboard): don't show false positive message for failed user creation
|
||||
- @nhost/react-apollo@5.0.3
|
||||
- @nhost/nextjs@1.13.8
|
||||
|
||||
## 0.11.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.2
|
||||
- @nhost/nextjs@1.13.7
|
||||
|
||||
## 0.11.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2cc18dcb: fix(dashboard): prevent permission editor dropdown from being always open
|
||||
|
||||
## 0.11.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3343a363: chore(dashboard): bump `@testing-library/react` to v14 and `@testing-library/dom` to v9
|
||||
- @nhost/react-apollo@5.0.1
|
||||
- @nhost/nextjs@1.13.6
|
||||
|
||||
## 0.11.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 87eda76e: chore(dashboard): bump `@types/react` to v18.0.28 and `@types/react-dom` to v18.0.11
|
||||
- 6f0ac570: feat(dashboard): show dashboard version in account menu
|
||||
|
||||
## 0.11.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bf1e4071: chore(dashboard): bump `react-is` version to `18.2.0`
|
||||
- Updated dependencies [bf1e4071]
|
||||
- Updated dependencies [5013213b]
|
||||
- @nhost/nextjs@1.13.5
|
||||
- @nhost/react-apollo@4.13.5
|
||||
|
||||
## 0.11.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a37a430b: fix(dashboard): don't break UI when deployments are unavailable
|
||||
- @nhost/react-apollo@4.13.4
|
||||
- @nhost/nextjs@1.13.4
|
||||
|
||||
## 0.11.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b970e68: fix(dashboard): fix header link color
|
||||
|
||||
## 0.11.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f33242f2: feat(dashboard): add new sign up, sign in and reset password pages
|
||||
|
||||
## 0.11.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e9c8909c: fix(dashboard): use correct theme color in dark mode
|
||||
|
||||
## 0.11.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo@1.6.3
|
||||
RUN yarn global add turbo@1.9.8
|
||||
COPY . .
|
||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||
|
||||
@@ -11,7 +11,7 @@ FROM node:16-alpine AS builder
|
||||
ARG TURBO_TOKEN
|
||||
ARG TURBO_TEAM
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk add --no-cache libc6-compat python3 make g++
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
@@ -19,12 +19,17 @@ 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__
|
||||
# placeholders for URLs, will be replaced on runtime by entrypoint script
|
||||
ENV NEXT_PUBLIC_NHOST_ADMIN_SECRET __NEXT_PUBLIC_NHOST_ADMIN_SECRET__
|
||||
ENV NEXT_PUBLIC_NHOST_AUTH_URL __NEXT_PUBLIC_NHOST_AUTH_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_FUNCTIONS_URL __NEXT_PUBLIC_NHOST_FUNCTIONS_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_GRAPHQL_URL __NEXT_PUBLIC_NHOST_GRAPHQL_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_STORAGE_URL __NEXT_PUBLIC_NHOST_STORAGE_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL __NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
|
||||
|
||||
RUN yarn global add pnpm@7.17.0
|
||||
RUN yarn global add pnpm@8.4.0
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-*.yaml .
|
||||
|
||||
@@ -35,8 +35,17 @@ You can connect the Nhost Dashboard to your locally running backend by setting t
|
||||
```bash
|
||||
NEXT_PUBLIC_ENV=dev
|
||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
NEXT_PUBLIC_NHOST_AUTH_URL=https://local.auth.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_FUNCTIONS_URL=https://local.functions.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_GRAPHQL_URL=https://local.graphql.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_STORAGE_URL=https://local.storage.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=https://local.hasura.nhost.run
|
||||
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=https://local.hasura.nhost.run/v1/migrations
|
||||
NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.nhost.run
|
||||
```
|
||||
|
||||
This will connect the Nhost Dashboard to your locally running Nhost backend.
|
||||
|
||||
### Storybook
|
||||
|
||||
Components are documented using [Storybook](https://storybook.js.org/). To run Storybook, run the following command:
|
||||
@@ -45,20 +54,38 @@ Components are documented using [Storybook](https://storybook.js.org/). To run S
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
### Full list of environment variables
|
||||
### General Environment Variables
|
||||
|
||||
| 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. |
|
||||
| Name | Description |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. This should be set to `dev` in most cases. |
|
||||
| `NEXT_PUBLIC_NHOST_ADMIN_SECRET` | Admin secret for Hasura. Default: `nhost-admin-secret` |
|
||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running or a self-hosted Nhost backend. Setting this to `true` will connect the Nhost Dashboard to the cloud environment. Default: `false` |
|
||||
|
||||
### Environment Variables for Local Development and Self-Hosting
|
||||
|
||||
| Name | Description |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `NEXT_PUBLIC_NHOST_AUTH_URL` | The URL of the Auth service. When working locally, point it to the Auth service started by the CLI. When self-hosting, point it to the self-hosted Auth service. |
|
||||
| `NEXT_PUBLIC_NHOST_FUNCTIONS_URL` | The URL of the Functions service. When working locally, point it to the Functions service started by the CLI. When self-hosting, point it to the self-hosted Functions service. |
|
||||
| `NEXT_PUBLIC_NHOST_GRAPHQL_URL` | The URL of the GraphQL service. When working locally, point it to the GraphQL service started by the CLI. When self-hosting, point it to the self-hosted GraphQL service. |
|
||||
| `NEXT_PUBLIC_NHOST_STORAGE_URL` | The URL of the Storage service. When working locally, point it to the Storage service started by the CLI. When self-hosting, point it to the self-hosted Storage service. |
|
||||
| `NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL` | The URL of the Hasura Console. When working locally, point it to the Hasura Console started by the CLI. When self-hosting, point it to the self-hosted Hasura Console. |
|
||||
| `NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL` | The URL of Hasura's Migrations service. When working locally, point it to the Migrations service started by the CLI. |
|
||||
| `NEXT_PUBLIC_NHOST_HASURA_API_URL` | The URL of Hasura's Schema and Metadata API. When working locally, point it to the Schema and Metadata API started by the CLI. When self-hosting, point it to the self-hosted Schema and Metadata API. |
|
||||
|
||||
### Other Environment Variables
|
||||
|
||||
| Name | Description |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
| `NEXT_PUBLIC_MAINTENANCE_ACTIVE` | Determines whether or not maintenance mode is active. |
|
||||
| `NEXT_PUBLIC_MAINTENANCE_END_DATE` | Date when maintenance mode will end. |
|
||||
| `NEXT_PUBLIC_MAINTENANCE_UNLOCK_SECRET` | Secret that can be used to bypass maintenance mode. |
|
||||
|
||||
## ESLint Rules
|
||||
|
||||
@@ -83,3 +110,22 @@ pnpm storybook
|
||||
| `@typescript-eslint/consistent-type-imports` | Enforces `import type { Type } from 'module'` syntax. It prevents false positive circular dependency errors. |
|
||||
| `@typescript-eslint/naming-convention` | Enforces a consistent naming convention. |
|
||||
| `no-restricted-imports` | Enforces absolute imports and consistent import paths for components from `src/components/ui` folder. |
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
End-to-end tests are written using [Playwright](https://playwright.dev/). To run the tests, run the following command:
|
||||
|
||||
```bash
|
||||
pnpm e2e
|
||||
```
|
||||
|
||||
Most of the tests require access to the Nhost test user. To run these tests, you need to set the following environment variables in `.env.test`:
|
||||
|
||||
```
|
||||
NHOST_TEST_DASHBOARD_URL=<test_dashboard_url>
|
||||
NHOST_TEST_USER_EMAIL=<test_user_email>
|
||||
NHOST_TEST_USER_PASSWORD=<test_user_password>
|
||||
NHOST_TEST_WORKSPACE_NAME=<test_workspace_name>
|
||||
NHOST_TEST_PROJECT_NAME=<test_project_name>
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET=<test_project_admin_secret>
|
||||
```
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
# 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}"
|
||||
# read URLs from env variables (with defaults)
|
||||
NEXT_PUBLIC_NHOST_ADMIN_SECRET="${NEXT_PUBLIC_NHOST_ADMIN_SECRET:-nhost-admin-secret}"
|
||||
NEXT_PUBLIC_NHOST_AUTH_URL="${NEXT_PUBLIC_NHOST_AUTH_URL:-http://localhost:1337/v1/auth}"
|
||||
NEXT_PUBLIC_NHOST_FUNCTIONS_URL="${NEXT_PUBLIC_NHOST_FUNCTIONS_URL:-http://localhost:1337/v1/functions}"
|
||||
NEXT_PUBLIC_NHOST_GRAPHQL_URL="${NEXT_PUBLIC_NHOST_GRAPHQL_URL:-http://localhost:1337/v1/graphql}"
|
||||
NEXT_PUBLIC_NHOST_STORAGE_URL="${NEXT_PUBLIC_NHOST_STORAGE_URL:-http://localhost:1337/v1/storage}"
|
||||
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL="${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL:-http://localhost:9695}"
|
||||
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL="${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL:-http://localhost:9693}"
|
||||
NEXT_PUBLIC_NHOST_HASURA_API_URL="${NEXT_PUBLIC_NHOST_HASURA_API_URL:-http://localhost:8080}"
|
||||
|
||||
# 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" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_ADMIN_SECRET__~${NEXT_PUBLIC_NHOST_ADMIN_SECRET}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_AUTH_URL__~${NEXT_PUBLIC_NHOST_AUTH_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_FUNCTIONS_URL__~${NEXT_PUBLIC_NHOST_FUNCTIONS_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_GRAPHQL_URL__~${NEXT_PUBLIC_NHOST_GRAPHQL_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_STORAGE_URL__~${NEXT_PUBLIC_NHOST_STORAGE_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} +
|
||||
|
||||
exec "$@"
|
||||
|
||||
50
dashboard/e2e/auth/ban-user.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import test, { expect } from '@playwright/test';
|
||||
|
||||
test('should be able to ban and unban a user', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /auth/i })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
|
||||
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
.click();
|
||||
await page.getByRole('button', { name: /actions/i }).click();
|
||||
await page.getByRole('menuitem', { name: /ban user/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(/user has been banned successfully./i),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('form').getByText(/^banned$/i)).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /actions/i }).click();
|
||||
await page.getByRole('menuitem', { name: /unban user/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(/user has been unbanned successfully./i),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('form').getByText(/^banned$/i)).not.toBeVisible();
|
||||
});
|
||||
65
dashboard/e2e/auth/create-user.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import test, { expect } from '@playwright/test';
|
||||
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /auth/i })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should create a user', async () => {
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not be able to create a user with an existing email', async () => {
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog').getByText(/email already in use/i),
|
||||
).toBeVisible();
|
||||
});
|
||||
96
dashboard/e2e/auth/delete-user.test.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import test, { expect } from '@playwright/test';
|
||||
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /auth/i })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should be able to delete a user', async () => {
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `More options for ${email}`, exact: true })
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: /delete user/i }).click();
|
||||
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /delete user/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(`Are you sure you want to delete the "${email}" user?`),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /delete/i, exact: true }).click();
|
||||
|
||||
await expect(page.getByRole('dialog')).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to delete a user from the details page', async () => {
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: /actions/i }).click();
|
||||
await page.getByRole('menuitem', { name: /delete user/i }).click();
|
||||
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /delete user/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(`Are you sure you want to delete the "${email}" user?`),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /delete/i, exact: true }).click();
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
103
dashboard/e2e/auth/verify-user.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /auth/i })
|
||||
.click();
|
||||
|
||||
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should be able to verify the email of a user', async () => {
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: /email verified/i }),
|
||||
).not.toBeChecked();
|
||||
await page.getByRole('checkbox', { name: /email verified/i }).check();
|
||||
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(/user settings have been updated successfully./i),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: /email verified/i }),
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
test('should be able to verify the phone number of a user', async () => {
|
||||
const email = generateTestEmail();
|
||||
const password = faker.internet.password();
|
||||
const phoneNumber = faker.phone.number();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: /phone number verified/i }),
|
||||
).toBeDisabled();
|
||||
|
||||
await page.getByRole('textbox', { name: /phone number/i }).fill(phoneNumber);
|
||||
await page.getByRole('checkbox', { name: /phone number verified/i }).check();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(/user settings have been updated successfully./i),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: /phone number/i }),
|
||||
).toHaveValue(phoneNumber);
|
||||
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: /phone number verified/i }),
|
||||
).toBeChecked();
|
||||
});
|
||||
280
dashboard/e2e/database/create-table.test.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject, prepareTable } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /database/i })
|
||||
.click();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should create a simple table', async () => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a table with unique constraints', async () => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text', unique: true },
|
||||
{ name: 'isbn', type: 'text', unique: true },
|
||||
],
|
||||
});
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a table with nullable columns', async () => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text', nullable: true },
|
||||
{ name: 'description', type: 'text', nullable: true },
|
||||
],
|
||||
});
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a table with an identity column', async () => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'int4' },
|
||||
{ name: 'title', type: 'text', nullable: true },
|
||||
{ name: 'description', type: 'text', nullable: true },
|
||||
],
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /identity/i }).click();
|
||||
await page.getByRole('option', { name: /id/i }).click();
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create table with foreign key constraint', async () => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const firstTableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: firstTableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const secondTableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: secondTableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'author_id', type: 'uuid' },
|
||||
],
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /add foreign key/i }).click();
|
||||
|
||||
// select column in current table
|
||||
await page
|
||||
.getByRole('button', { name: /column/i })
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole('option', { name: /author_id/i }).click();
|
||||
|
||||
// select reference schema
|
||||
await page.getByRole('button', { name: /schema/i }).click();
|
||||
await page.getByRole('option', { name: /public/i }).click();
|
||||
|
||||
// select reference table
|
||||
await page.getByRole('button', { name: /table/i }).click();
|
||||
await page.getByRole('option', { name: firstTableName, exact: true }).click();
|
||||
|
||||
// select reference column
|
||||
await page
|
||||
.getByRole('button', { name: /column/i })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole('option', { name: /id/i }).click();
|
||||
|
||||
await page.getByRole('button', { name: /add/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(`public.${firstTableName}.id`, { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: secondTableName, exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not be able to create a table with a name that already exists', async () => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'author_id', type: 'uuid' },
|
||||
],
|
||||
});
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(/error: a table with this name already exists/i),
|
||||
).toBeVisible();
|
||||
});
|
||||
165
dashboard/e2e/database/delete-table.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { deleteTable, openProject, prepareTable } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('navigation', { name: /main navigation/i })
|
||||
.getByRole('link', { name: /database/i })
|
||||
.click();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should delete a table', async () => {
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await deleteTable({
|
||||
page,
|
||||
name: tableName,
|
||||
});
|
||||
|
||||
// navigate to next URL
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/**`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should not be able to delete a table if other tables have foreign keys referencing it', async () => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const firstTableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: firstTableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${firstTableName}`,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const secondTableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: secondTableName,
|
||||
primaryKey: 'id',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'author_id', type: 'uuid' },
|
||||
],
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /add foreign key/i }).click();
|
||||
|
||||
// select column in current table
|
||||
await page
|
||||
.getByRole('button', { name: /column/i })
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole('option', { name: /author_id/i }).click();
|
||||
|
||||
// select reference schema
|
||||
await page.getByRole('button', { name: /schema/i }).click();
|
||||
await page.getByRole('option', { name: /public/i }).click();
|
||||
|
||||
// select reference table
|
||||
await page.getByRole('button', { name: /table/i }).click();
|
||||
await page.getByRole('option', { name: firstTableName, exact: true }).click();
|
||||
|
||||
// select reference column
|
||||
await page
|
||||
.getByRole('button', { name: /column/i })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole('option', { name: /id/i }).click();
|
||||
|
||||
await page.getByRole('button', { name: /add/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(`public.${firstTableName}.id`, { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// create table
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${secondTableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: secondTableName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// try to delete the first table that is referenced by the second table
|
||||
await deleteTable({
|
||||
page,
|
||||
name: firstTableName,
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
/constraint [a-zA-Z_]+ on table [a-zA-Z_]+ depends on table [a-zA-Z_]+/i,
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
48
dashboard/e2e/env.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import slugify from 'slugify';
|
||||
|
||||
/**
|
||||
* URL of the dashboard to test against.
|
||||
*/
|
||||
export const TEST_DASHBOARD_URL = process.env.NHOST_TEST_DASHBOARD_URL;
|
||||
|
||||
/**
|
||||
* Name of the workspace to test against.
|
||||
*/
|
||||
export const TEST_WORKSPACE_NAME = process.env.NHOST_TEST_WORKSPACE_NAME;
|
||||
|
||||
/**
|
||||
* Slugified name of the workspace to test against.
|
||||
*/
|
||||
export const TEST_WORKSPACE_SLUG = slugify(TEST_WORKSPACE_NAME, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Name of the project to test against.
|
||||
*/
|
||||
export const TEST_PROJECT_NAME = process.env.NHOST_TEST_PROJECT_NAME;
|
||||
|
||||
/**
|
||||
* Slugified name of the project to test against.
|
||||
*/
|
||||
export const TEST_PROJECT_SLUG = slugify(TEST_PROJECT_NAME, {
|
||||
lower: true,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Hasura admin secret of the test project to use.
|
||||
*/
|
||||
export const TEST_PROJECT_ADMIN_SECRET =
|
||||
process.env.NHOST_TEST_PROJECT_ADMIN_SECRET;
|
||||
|
||||
/**
|
||||
* Email of the test account to use.
|
||||
*/
|
||||
export const TEST_USER_EMAIL = process.env.NHOST_TEST_USER_EMAIL;
|
||||
|
||||
/**
|
||||
* Password of the test account to use.
|
||||
*/
|
||||
export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD;
|
||||
122
dashboard/e2e/overview/overview.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_NAME,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject } from '@/e2e/utils';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
|
||||
await page.goto('/');
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should show a sidebar with menu items', async () => {
|
||||
const navLocator = page.getByRole('navigation', { name: /main navigation/i });
|
||||
await expect(navLocator).toBeVisible();
|
||||
await expect(navLocator.getByRole('list').getByRole('listitem')).toHaveCount(
|
||||
11,
|
||||
);
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /overview/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /database/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /graphql/i }),
|
||||
).toBeVisible();
|
||||
await expect(navLocator.getByRole('link', { name: /hasura/i })).toBeVisible();
|
||||
await expect(navLocator.getByRole('link', { name: /auth/i })).toBeVisible();
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /storage/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /deployments/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /backups/i }),
|
||||
).toBeVisible();
|
||||
await expect(navLocator.getByRole('link', { name: /logs/i })).toBeVisible();
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /metrics/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /settings/i }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show a header with a logo, the workspace name, and the project name', async () => {
|
||||
await expect(
|
||||
page.getByRole('banner').getByRole('link', { name: TEST_WORKSPACE_NAME }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('banner').getByRole('link', { name: TEST_PROJECT_NAME }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show the project's name, the Upgrade button and the Settings button", async () => {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: TEST_PROJECT_NAME }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/starter/i)).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /upgrade/i })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('main').getByRole('link', { name: /settings/i }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show the project's region and subdomain", async () => {
|
||||
await expect(page.locator('p:has-text("Region") + div p').nth(0)).toHaveText(
|
||||
/frankfurt \(eu-central-1\)/i,
|
||||
);
|
||||
await expect(
|
||||
page.locator('p:has-text("Subdomain") + div p').nth(0),
|
||||
).toHaveText(/[a-z]{20}/i);
|
||||
});
|
||||
|
||||
test('should not have a GitHub repository connected', async () => {
|
||||
await expect(
|
||||
page.getByRole('button', { name: /connect to github/i }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show metrics', async () => {
|
||||
await expect(page.getByText(/cpu usage seconds\d+/i)).toBeVisible();
|
||||
await expect(page.getByText(/total requests\d+/i)).toBeVisible();
|
||||
await expect(page.getByText(/function invocations\d+/i)).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/egress volume\d+(\.\d+)? [a-zA-Z]+/i),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/logs\d+(\.\d+)? [a-zA-Z]+/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show proper limits for the free project', async () => {
|
||||
await expect(
|
||||
page.getByText(/database\d+(\.\d+)? [a-zA-Z]+ of \d+(\.\d+)? [a-zA-Z]+/i),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText(/storage\d+(\.\d+)? [a-zA-Z]+ of \d+(\.\d+)? [a-zA-Z]+/i),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByText(/users[0-9]+ of [0-9]+/i)).toBeVisible();
|
||||
|
||||
await expect(page.getByText(/functions[0-9]+ of [0-9]+/i)).toBeVisible();
|
||||
});
|
||||
20
dashboard/e2e/setup/auth.setup.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
TEST_DASHBOARD_URL,
|
||||
TEST_USER_EMAIL,
|
||||
TEST_USER_PASSWORD,
|
||||
} from '@/e2e/env';
|
||||
import { test as setup } from '@playwright/test';
|
||||
|
||||
setup('authenticate user', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForURL('/signin');
|
||||
await page.getByRole('link', { name: /continue with email/i }).click();
|
||||
|
||||
await page.waitForURL('/signin/email');
|
||||
await page.getByLabel('Email').fill(TEST_USER_EMAIL);
|
||||
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
|
||||
await page.waitForURL(TEST_DASHBOARD_URL);
|
||||
await page.context().storageState({ path: 'e2e/.auth/user.json' });
|
||||
});
|
||||
189
dashboard/e2e/utils.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Open a project by navigating to the project's overview page.
|
||||
*
|
||||
* @param page - The Playwright page object.
|
||||
* @param workspaceSlug - The slug of the workspace that contains the project.
|
||||
* @param projectSlug - The slug of the project to open.
|
||||
* @param projectName - The name of the project to open.
|
||||
* @returns A promise that resolves when the project is opened.
|
||||
*/
|
||||
export async function openProject({
|
||||
page,
|
||||
projectName,
|
||||
workspaceSlug,
|
||||
projectSlug,
|
||||
}: {
|
||||
page: Page;
|
||||
workspaceSlug: string;
|
||||
projectSlug: string;
|
||||
projectName: string;
|
||||
}) {
|
||||
await page.getByRole('link', { name: projectName }).click();
|
||||
await page.waitForURL(`/${workspaceSlug}/${projectSlug}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a table by filling out the form.
|
||||
*
|
||||
* @param page - The Playwright page object.
|
||||
* @param name - The name of the table to create.
|
||||
* @param columns - The columns to create in the table.
|
||||
* @returns A promise that resolves when the table is prepared.
|
||||
*/
|
||||
export async function prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey,
|
||||
columns,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
primaryKey: string;
|
||||
columns: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
nullable?: boolean;
|
||||
unique?: boolean;
|
||||
defaultValue?: string;
|
||||
}>;
|
||||
}) {
|
||||
if (!columns.some(({ name }) => name === primaryKey)) {
|
||||
throw new Error('Primary key must be one of the columns.');
|
||||
}
|
||||
|
||||
await page.getByRole('textbox', { name: /name/i }).first().fill(tableName);
|
||||
|
||||
await Promise.all(
|
||||
columns.map(
|
||||
async (
|
||||
{ name: columnName, type, nullable, unique, defaultValue },
|
||||
index,
|
||||
) => {
|
||||
// set name
|
||||
await page.getByPlaceholder(/name/i).nth(index).fill(columnName);
|
||||
|
||||
// set type
|
||||
await page
|
||||
.getByRole('table')
|
||||
.getByRole('combobox', { name: /type/i })
|
||||
.nth(index)
|
||||
.type(type);
|
||||
await page
|
||||
.getByRole('table')
|
||||
.getByRole('option', { name: type })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// optionally set default value
|
||||
if (defaultValue) {
|
||||
await page
|
||||
.getByRole('table')
|
||||
.getByRole('combobox', { name: /default value/i })
|
||||
.nth(index)
|
||||
.type(defaultValue);
|
||||
await page
|
||||
.getByRole('table')
|
||||
.getByRole('option', { name: defaultValue })
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
|
||||
// optionally check unique
|
||||
if (unique) {
|
||||
await page
|
||||
.getByRole('checkbox', { name: /unique/i })
|
||||
.nth(index)
|
||||
.check();
|
||||
}
|
||||
|
||||
// optionally check nullable
|
||||
if (nullable) {
|
||||
await page
|
||||
.getByRole('checkbox', { name: /nullable/i })
|
||||
.nth(index)
|
||||
.check();
|
||||
}
|
||||
|
||||
// add new column if not last
|
||||
if (index < columns.length - 1) {
|
||||
await page.getByRole('button', { name: /add column/i }).click();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// select the first column as primary key
|
||||
await page.getByRole('button', { name: /primary key/i }).click();
|
||||
await page.getByRole('option', { name: primaryKey, exact: true }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a table with the given name.
|
||||
*
|
||||
* @param page - The Playwright page object.
|
||||
* @param name - The name of the table to delete.
|
||||
* @returns A promise that resolves when the table is deleted.
|
||||
*/
|
||||
export async function deleteTable({
|
||||
page,
|
||||
name,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
}) {
|
||||
const tableLink = page.getByRole('link', {
|
||||
name,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await tableLink.hover();
|
||||
await page
|
||||
.getByRole('listitem')
|
||||
.filter({ hasText: name })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /delete table/i }).click();
|
||||
await page.getByRole('button', { name: /delete/i }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
*
|
||||
* @param page - The Playwright page object.
|
||||
* @param email - The email of the user to create.
|
||||
* @param password - The password of the user to create.
|
||||
* @returns A promise that resolves when the user is created.
|
||||
*/
|
||||
export async function createUser({
|
||||
page,
|
||||
email,
|
||||
password,
|
||||
}: {
|
||||
page: Page;
|
||||
email: string;
|
||||
password: string;
|
||||
}) {
|
||||
await page
|
||||
.getByRole('button', { name: /create user/i })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page.getByRole('textbox', { name: /email/i }).fill(email);
|
||||
await page.getByRole('textbox', { name: /password/i }).fill(password);
|
||||
await page.getByRole('button', { name: /create/i, exact: true }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a test email address with the given prefix (if provided).
|
||||
*
|
||||
* @param prefix - The prefix to use for the email address. (Default: `Nhost_Test_`)
|
||||
*/
|
||||
export function generateTestEmail(prefix: string = 'Nhost_Test_') {
|
||||
const email = faker.internet.email();
|
||||
|
||||
return [prefix, email].join('');
|
||||
}
|
||||
66
dashboard/global-teardown.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
TEST_DASHBOARD_URL,
|
||||
TEST_PROJECT_ADMIN_SECRET,
|
||||
TEST_PROJECT_NAME,
|
||||
TEST_PROJECT_SLUG,
|
||||
TEST_WORKSPACE_SLUG,
|
||||
} from '@/e2e/env';
|
||||
import { openProject } from '@/e2e/utils';
|
||||
import { chromium } from '@playwright/test';
|
||||
|
||||
async function globalTeardown() {
|
||||
const browser = await chromium.launch();
|
||||
|
||||
const context = await browser.newContext({
|
||||
baseURL: TEST_DASHBOARD_URL,
|
||||
storageState: 'e2e/.auth/user.json',
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await openProject({
|
||||
page,
|
||||
projectName: TEST_PROJECT_NAME,
|
||||
workspaceSlug: TEST_WORKSPACE_SLUG,
|
||||
projectSlug: TEST_PROJECT_SLUG,
|
||||
});
|
||||
|
||||
const pagePromise = context.waitForEvent('page');
|
||||
|
||||
await page.getByRole('link', { name: /hasura/i }).click();
|
||||
await page.getByRole('link', { name: /open hasura/i }).click();
|
||||
|
||||
const hasuraPage = await pagePromise;
|
||||
await hasuraPage.waitForLoadState();
|
||||
|
||||
const adminSecretInput = hasuraPage.getByPlaceholder(/enter admin-secret/i);
|
||||
|
||||
// note: a more ideal way would be to paste from clipboard, but Playwright
|
||||
// doesn't support that yet
|
||||
await adminSecretInput.fill(TEST_PROJECT_ADMIN_SECRET);
|
||||
await adminSecretInput.press('Enter');
|
||||
|
||||
// note: getByRole doesn't work here
|
||||
await hasuraPage.locator('a', { hasText: /data/i }).click();
|
||||
await hasuraPage.getByRole('link', { name: /sql/i }).click();
|
||||
|
||||
await hasuraPage.getByRole('textbox').fill(`
|
||||
DO $$ DECLARE
|
||||
tablename text;
|
||||
BEGIN
|
||||
FOR tablename IN
|
||||
SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(tablename) || ' CASCADE';
|
||||
END LOOP;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await hasuraPage.getByRole('button', { name: /run!/i }).click();
|
||||
await hasuraPage.getByText(/sql executed!/i).waitFor();
|
||||
}
|
||||
|
||||
export default globalTeardown;
|
||||
@@ -1,5 +1,5 @@
|
||||
schema:
|
||||
- http://localhost:1337/v1/graphql:
|
||||
- https://local.graphql.nhost.run/v1:
|
||||
headers:
|
||||
x-hasura-admin-secret: nhost-admin-secret
|
||||
generates:
|
||||
|
||||
@@ -2,6 +2,7 @@ const path = require('path');
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
const { version } = require('./package.json');
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
@@ -10,6 +11,9 @@ module.exports = withBundleAnalyzer({
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
version,
|
||||
},
|
||||
eslint: {
|
||||
dirs: ['src'],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.11.6",
|
||||
"version": "0.16.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -8,16 +8,17 @@
|
||||
"build": "next build --no-lint",
|
||||
"analyze": "ANALYZE=true pnpm build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings 2",
|
||||
"lint": "next lint --max-warnings 0",
|
||||
"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 -s public",
|
||||
"build-storybook": "build-storybook"
|
||||
"build-storybook": "build-storybook",
|
||||
"e2e": "npx playwright@1.33.0 install --with-deps && playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.3",
|
||||
"@apollo/client": "^3.7.10",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@emotion/cache": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
@@ -25,11 +26,11 @@
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@graphiql/react": "^0.15.0",
|
||||
"@graphiql/toolkit": "^0.8.0",
|
||||
"@graphiql/react": "^0.17.0",
|
||||
"@graphiql/toolkit": "^0.8.2",
|
||||
"@headlessui/react": "^1.6.5",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@hookform/resolvers": "^3.0.0",
|
||||
"@mui/base": "^5.0.0-alpha.106",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/system": "^5.10.14",
|
||||
@@ -37,58 +38,57 @@
|
||||
"@nhost/nextjs": "workspace:*",
|
||||
"@nhost/react-apollo": "workspace:*",
|
||||
"@segment/snippet": "^4.15.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/react-stripe-js": "^2.0.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@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",
|
||||
"clsx": "^1.2.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"date-fns": "^2.29.3",
|
||||
"generate-password": "^1.7.0",
|
||||
"graphiql": "^2.2.0",
|
||||
"graphiql": "^2.4.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^4.3.0",
|
||||
"graphql-request": "^6.0.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"just-kebab-case": "^4.1.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "^12.3.1",
|
||||
"next-seo": "^5.14.1",
|
||||
"next-seo": "^6.0.0",
|
||||
"node-pg-format": "^1.3.5",
|
||||
"pluralize": "^8.0.0",
|
||||
"prettysize": "^2.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-error-boundary": "^4.0.0",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-is": "17.0.2",
|
||||
"react-is": "18.2.0",
|
||||
"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",
|
||||
"sharp": "^0.32.0",
|
||||
"slugify": "^1.6.5",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^1.8.0",
|
||||
"utility-types": "^3.10.0",
|
||||
"validator": "^13.7.0",
|
||||
"yup": "^0.32.11",
|
||||
"yup": "^1.0.2",
|
||||
"yup-password": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@graphql-codegen/cli": "^2.8.0",
|
||||
"@graphql-codegen/typescript": "^2.7.1",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@graphql-codegen/cli": "^3.0.0",
|
||||
"@graphql-codegen/typescript": "^3.0.0",
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.1",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||
"@next/bundle-analyzer": "^12.3.1",
|
||||
"@playwright/test": "^1.33.0",
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.14",
|
||||
"@storybook/addon-interactions": "^6.5.14",
|
||||
@@ -98,26 +98,28 @@
|
||||
"@storybook/manager-webpack5": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.0.25",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/validator": "^13.7.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"@vitest/coverage-c8": "^0.27.0",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitest/coverage-c8": "^0.31.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"csstype": "^3.0.10",
|
||||
"dotenv": "^16.0.3",
|
||||
"encoding": "^0.1.13",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
@@ -127,25 +129,25 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jsdom": "^21.0.0",
|
||||
"jsdom": "^22.0.0",
|
||||
"lint-staged": ">=13",
|
||||
"msw": "^0.49.0",
|
||||
"msw": "^1.0.1",
|
||||
"msw-storybook-addon": "^1.6.3",
|
||||
"node-fetch": "^3.3.0",
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"snake-case": "^3.0.4",
|
||||
"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": "^4.0.2",
|
||||
"vite-tsconfig-paths": "^4.0.3",
|
||||
"vitest": "^0.27.0",
|
||||
"webpack": "^5.75.0"
|
||||
"vitest": "^0.31.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -162,4 +164,4 @@
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
dashboard/playwright.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '.env.test') });
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
globalTeardown: require.resolve('./global-teardown'),
|
||||
use: {
|
||||
actionTimeout: 0,
|
||||
trace: 'on-first-retry',
|
||||
baseURL: process.env.NHOST_TEST_DASHBOARD_URL,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'setup',
|
||||
testMatch: ['**/setup/*.setup.ts'],
|
||||
},
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: 'e2e/.auth/user.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
<svg width="142" height="48" viewBox="0 0 142 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.985 22.8273H83.1731V17.1251H86.9756V31.7529H83.1731V26.0506H75.985V31.7529H72.3164V17.1251H75.985V22.8273ZM101.48 17.1251H95.6099C93.9842 17.1251 92.9947 17.4319 92.2458 18.1792C91.4999 18.9235 91.2137 19.8684 91.2137 21.5362V27.3478C91.2137 29.0157 91.4999 29.9575 92.2458 30.7048C92.9917 31.4522 93.9811 31.759 95.6099 31.759H101.48C103.105 31.759 104.095 31.4522 104.844 30.7048C105.593 29.9605 105.876 29.0157 105.876 27.3478V21.5362C105.876 19.8684 105.59 18.9266 104.844 18.1792C104.095 17.4319 103.105 17.1251 101.48 17.1251ZM102.204 27.4329C102.204 28.2896 101.918 28.5296 100.886 28.5296H96.2036C95.1715 28.5296 94.8853 28.2896 94.8853 27.4329V21.4451C94.8853 20.5884 95.1715 20.3484 96.2036 20.3484H100.886C101.942 20.3484 102.204 20.5671 102.204 21.4451V27.4329ZM114.972 22.9792H120.224C121.85 22.9792 122.861 23.2861 123.588 24.0334C124.246 24.6927 124.62 25.7651 124.62 26.9286V27.8066C124.62 28.9701 124.246 30.0213 123.588 30.7018C122.861 31.4491 121.871 31.756 120.224 31.756H110.202V28.5327H119.609C120.641 28.5327 120.927 28.2927 120.927 27.4359V26.9985C120.927 26.1418 120.641 25.9018 119.609 25.9018H114.357C112.729 25.9018 111.718 25.5919 110.993 24.8476C110.336 24.1884 109.961 23.1159 109.961 21.9524V21.0744C109.961 19.9109 110.333 18.8597 110.993 18.1792C111.721 17.4319 112.707 17.1251 114.357 17.1251H123.856V20.3484H114.972C113.94 20.3484 113.654 20.5884 113.654 21.4451V21.8825C113.654 22.7392 113.94 22.9792 114.972 22.9792ZM126.946 20.3484V17.1251H141.891V20.3484H136.265V31.7529H132.594V20.3484H126.946Z" fill="#21324B"/>
|
||||
<path d="M63.5321 17.0096H57.6624C56.0366 17.0096 55.0471 17.3164 54.2982 18.0638C53.5523 18.8081 53.2661 19.7529 53.2661 21.4177V23.6385V24.322V31.7499H56.9378V24.3251V23.6415V21.3327C56.9378 20.4759 57.224 20.2359 58.256 20.2359H62.9385C63.9949 20.2359 64.2567 20.4547 64.2567 21.3327V23.6415V24.3251V31.7529H67.9284V24.3251V23.6415V21.4208C67.9284 19.7529 67.6422 18.8111 66.8963 18.0668C66.1504 17.3164 65.1609 17.0096 63.5321 17.0096Z" fill="#0052CD"/>
|
||||
<g clip-path="url(#clip1)">
|
||||
<path d="M40.8114 10.2866L24.0819 0.647089C22.581 -0.215696 20.7178 -0.215696 19.2138 0.647089C17.7129 1.51291 16.7813 3.12304 16.7813 4.85165V6.10937L15.6913 5.48051C14.1904 4.61772 12.3272 4.61772 10.8232 5.48051C9.32224 6.34633 8.39063 7.95646 8.39063 9.6881V10.9458L7.3007 10.317C5.79976 9.45418 3.93653 9.45418 2.43255 10.317C0.931612 11.1828 0 12.7929 0 14.5246V44.7281C0 45.597 0.505383 46.4051 1.29086 46.7818C2.0733 47.1615 3.02318 47.0582 3.70515 46.5205L12.0014 39.9919L24.7944 47.362C25.1475 47.5656 25.5433 47.6658 25.9391 47.6658C26.3349 47.6658 26.7307 47.5625 27.0838 47.362C27.7901 46.9549 28.2285 46.1985 28.2285 45.3843V27.2081C28.2285 24.2248 26.6211 21.4481 24.0332 19.9565L19.8379 17.5382V4.85468C19.8379 4.21063 20.185 3.60911 20.7452 3.28709C21.3054 2.96506 21.9995 2.96506 22.5597 3.28709L39.2892 12.9235C40.9363 13.8714 41.9592 15.6425 41.9592 17.5382V40.1711C41.9592 40.8152 41.6121 41.4167 41.052 41.7387L36.6192 44.2937V22.3716C36.6192 19.3884 35.0117 16.6116 32.4239 15.12L22.1243 9.18684V12.7018L30.8986 17.757C32.5456 18.7048 33.5686 20.4729 33.5686 22.3716V45.6091C33.5686 46.4203 34.007 47.1797 34.7133 47.5868C35.0665 47.7904 35.4623 47.8906 35.8581 47.8906C36.2538 47.8906 36.6496 47.7873 37.0028 47.5868L42.5803 44.3727C44.0812 43.5068 45.0128 41.8967 45.0128 40.1651V17.5322C45.0068 14.558 43.3993 11.7782 40.8114 10.2866ZM22.5019 22.5934C24.1489 23.5413 25.1719 25.3094 25.1719 27.2081V44.0689L14.577 37.9656L17.9777 35.2921C19.156 34.3656 19.8318 32.9772 19.8318 31.4795V21.0592L22.5019 22.5934ZM16.7813 19.2972V31.4734C16.7813 32.0324 16.5286 32.5519 16.0902 32.8952L3.05058 43.1544V14.5215C3.05058 13.8775 3.39766 13.2759 3.95784 12.9539C4.51803 12.6319 5.21217 12.6319 5.77236 12.9539L8.39063 14.4608V36.0456L11.4412 33.6456V9.6881C11.4412 9.04405 11.7883 8.44253 12.3485 8.12051C12.9087 7.79848 13.6028 7.79848 14.163 8.12051L16.7813 9.62734V15.7792L13.7307 14.0203V17.5382L16.7813 19.2972Z" fill="#0052CD"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="141.873" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1">
|
||||
<rect width="45.0128" height="47.8906" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
12
dashboard/public/assets/brands/azuread.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="150" height="30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M123.477 14.573c0-2.369 1.633-3.936 3.759-3.936s3.758 1.567 3.758 3.936c0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.759-1.604-3.759-3.936Zm4.252 7.361c3.758 0 7.023-3.134 7.023-7.398 0-4.263-3.265-7.397-7.023-7.397-2.468 0-3.91 1.42-3.91 1.42V2h-3.797v19.789h2.772l.645-1.422c.038 0 1.671 1.567 4.29 1.567Zm-14.73-7.361c0 2.369-1.632 3.936-3.758 3.936-2.127 0-3.759-1.567-3.759-3.936 0-2.369 1.632-3.936 3.759-3.936 2.126 0 3.758 1.567 3.758 3.936Zm-4.252 7.361c2.62 0 4.252-1.567 4.252-1.567l.645 1.422h2.772V7.357h-2.772l-.645 1.421s-1.632-1.567-4.252-1.567c-3.759 0-7.024 3.134-7.024 7.398 0 4.264 3.265 7.325 7.024 7.325ZM98.952 2h-3.758v19.789h3.758V2Zm-7.365 5.175c-2.278 0-3.606 1.713-3.606 1.713l-.646-1.567h-2.771v14.431h3.758V14.1c0-1.895 1.29-3.134 3.113-3.134.835 0 1.291.146 1.291.146V7.357s-.493-.182-1.139-.182Zm-17.008 3.462c1.974 0 3.265 1.24 3.607 2.843h-7.214c.304-1.604 1.633-2.843 3.607-2.843Zm2.771 6.742s-.835 1.093-2.771 1.093-3.113-1.093-3.417-2.514H81.64s.152-.802.152-1.421c0-4.41-3.265-7.399-7.213-7.399-4.1 0-7.517 3.28-7.517 7.399 0 4.118 3.417 7.397 7.517 7.397 3.417 0 5.885-2.368 6.72-4.555H77.35Zm-23.575-2.806c0-2.369 1.632-3.936 3.758-3.936 2.126 0 3.758 1.567 3.758 3.936 0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.758-1.604-3.758-3.936Zm4.252-7.398c-2.62 0-4.252 1.567-4.252 1.567l-.646-1.421h-2.771v19.46h3.758v-6.268s1.48 1.421 3.91 1.421c3.759 0 7.024-3.134 7.024-7.398 0-4.263-3.265-7.361-7.023-7.361ZM39.081 21.169l-.341.802c-.456 1.093-1.291 1.895-2.962 1.895v2.988s.646.146 1.291.146c2.468 0 3.759-1.239 5.24-4.701.341-.802 6.036-14.905 6.036-14.905h-3.873l-3.416 9.438-3.797-9.475h-3.91l5.732 13.812ZM16 4.515v17.274h3.91v-7.216h7.365v7.216h3.91V4.515h-3.91v6.596H19.91V4.515H16Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="150" height="30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M123.477 14.573c0-2.369 1.633-3.936 3.759-3.936s3.758 1.567 3.758 3.936c0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.759-1.604-3.759-3.936Zm4.252 7.361c3.758 0 7.023-3.134 7.023-7.398 0-4.263-3.265-7.397-7.023-7.397-2.468 0-3.91 1.42-3.91 1.42V2h-3.797v19.789h2.772l.645-1.422c.038 0 1.671 1.567 4.29 1.567Zm-14.73-7.361c0 2.369-1.632 3.936-3.758 3.936-2.127 0-3.759-1.567-3.759-3.936 0-2.369 1.632-3.936 3.759-3.936 2.126 0 3.758 1.567 3.758 3.936Zm-4.252 7.361c2.62 0 4.252-1.567 4.252-1.567l.645 1.422h2.772V7.357h-2.772l-.645 1.421s-1.632-1.567-4.252-1.567c-3.759 0-7.024 3.134-7.024 7.398 0 4.264 3.265 7.325 7.024 7.325ZM98.952 2h-3.758v19.789h3.758V2Zm-7.365 5.175c-2.278 0-3.606 1.713-3.606 1.713l-.646-1.567h-2.771v14.431h3.758V14.1c0-1.895 1.29-3.134 3.113-3.134.835 0 1.291.146 1.291.146V7.357s-.493-.182-1.139-.182Zm-17.008 3.462c1.974 0 3.265 1.24 3.607 2.843h-7.214c.304-1.604 1.633-2.843 3.607-2.843Zm2.771 6.742s-.835 1.093-2.771 1.093-3.113-1.093-3.417-2.514H81.64s.152-.802.152-1.421c0-4.41-3.265-7.399-7.213-7.399-4.1 0-7.517 3.28-7.517 7.399 0 4.118 3.417 7.397 7.517 7.397 3.417 0 5.885-2.368 6.72-4.555H77.35Zm-23.575-2.806c0-2.369 1.632-3.936 3.758-3.936 2.126 0 3.758 1.567 3.758 3.936 0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.758-1.604-3.758-3.936Zm4.252-7.398c-2.62 0-4.252 1.567-4.252 1.567l-.646-1.421h-2.771v19.46h3.758v-6.268s1.48 1.421 3.91 1.421c3.759 0 7.024-3.134 7.024-7.398 0-4.263-3.265-7.361-7.023-7.361ZM39.081 21.169l-.341.802c-.456 1.093-1.291 1.895-2.962 1.895v2.988s.646.146 1.291.146c2.468 0 3.759-1.239 5.24-4.701.341-.802 6.036-14.905 6.036-14.905h-3.873l-3.416 9.438-3.797-9.475h-3.91l5.732 13.812ZM16 4.515v17.274h3.91v-7.216h7.365v7.216h3.91V4.515h-3.91v6.596H19.91V4.515H16Z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="48" fill="none"><g clip-path="url(#a)"><path fill="#fff" fill-rule="evenodd" d="M75.985 22.827h7.188v-5.702h3.803v14.628h-3.803V26.05h-7.188v5.702h-3.669V17.125h3.669v5.702Zm25.495-5.702h-5.87c-1.626 0-2.615.307-3.364 1.054-.746.745-1.032 1.69-1.032 3.357v5.812c0 1.668.286 2.61 1.032 3.357.746.747 1.735 1.054 3.364 1.054h5.87c1.625 0 2.615-.307 3.364-1.054.749-.744 1.032-1.69 1.032-3.357v-5.812c0-1.668-.286-2.61-1.032-3.357-.749-.747-1.739-1.054-3.364-1.054Zm.724 10.308c0 .857-.286 1.097-1.318 1.097h-4.682c-1.032 0-1.319-.24-1.319-1.097v-5.988c0-.857.286-1.097 1.319-1.097h4.682c1.056 0 1.318.22 1.318 1.097v5.988Zm12.768-4.454h5.252c1.626 0 2.637.307 3.364 1.054.658.66 1.032 1.732 1.032 2.896v.878c0 1.163-.374 2.214-1.032 2.895-.727.747-1.717 1.054-3.364 1.054h-10.022v-3.223h9.407c1.032 0 1.318-.24 1.318-1.097v-.438c0-.856-.286-1.096-1.318-1.096h-5.252c-1.628 0-2.639-.31-3.364-1.054-.657-.66-1.032-1.732-1.032-2.896v-.878c0-1.163.372-2.214 1.032-2.895.728-.747 1.714-1.054 3.364-1.054h9.499v3.223h-8.884c-1.032 0-1.318.24-1.318 1.097v.438c0 .856.286 1.096 1.318 1.096Zm11.974-2.63v-3.224h14.945v3.223h-5.626v11.405h-3.671V20.348h-5.648Z" clip-rule="evenodd"/><path fill="#3888ff" d="M63.532 17.01h-5.87c-1.625 0-2.615.306-3.364 1.054-.746.744-1.032 1.689-1.032 3.354V31.75h3.672V21.332c0-.856.286-1.096 1.318-1.096h4.682c1.057 0 1.319.219 1.319 1.097v10.42h3.671V21.42c0-1.668-.286-2.61-1.032-3.354-.746-.75-1.735-1.057-3.364-1.057Z"/><g clip-path="url(#b)"><path fill="#3888ff" d="M40.811 10.287 24.081.647a4.901 4.901 0 0 0-4.867 0 4.868 4.868 0 0 0-2.433 4.205v1.257l-1.09-.628a4.901 4.901 0 0 0-4.868 0 4.869 4.869 0 0 0-2.432 4.207v1.258l-1.09-.629a4.901 4.901 0 0 0-4.868 0A4.869 4.869 0 0 0 0 14.525v30.203a2.29 2.29 0 0 0 1.29 2.054c.783.38 1.733.276 2.415-.261l8.296-6.53 12.793 7.371a2.29 2.29 0 0 0 1.145.304 2.288 2.288 0 0 0 2.29-2.281V27.207a8.392 8.392 0 0 0-4.196-7.252l-4.195-2.418V4.855a1.813 1.813 0 0 1 2.722-1.568l16.73 9.637a5.342 5.342 0 0 1 2.67 4.614v22.633c0 .644-.348 1.246-.908 1.568l-4.433 2.555V22.372a8.393 8.393 0 0 0-4.195-7.252l-10.3-5.933v3.515l8.775 5.055a5.338 5.338 0 0 1 2.67 4.615v23.237c0 .811.438 1.57 1.144 1.978a2.29 2.29 0 0 0 1.145.304c.396 0 .792-.104 1.145-.304l5.577-3.214a4.87 4.87 0 0 0 2.433-4.208V17.532a8.412 8.412 0 0 0-4.202-7.245Zm-18.31 12.306a5.339 5.339 0 0 1 2.67 4.615v16.86l-10.594-6.102 3.4-2.674a4.82 4.82 0 0 0 1.855-3.813V21.06l2.67 1.534Zm-5.72-3.296v12.176c0 .56-.252 1.079-.69 1.422L3.05 43.155V14.52a1.814 1.814 0 0 1 2.721-1.567l2.619 1.507v21.585l3.05-2.4V9.688a1.813 1.813 0 0 1 2.722-1.567l2.618 1.506v6.152l-3.05-1.759v3.518l3.05 1.76Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h141.873v48H0z"/></clipPath><clipPath id="b"><path fill="#fff" d="M0 0h45.013v47.891H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="48" fill="none"><g clip-path="url(#a)"><path fill="#21324B" fill-rule="evenodd" d="M75.985 22.827h7.188v-5.702h3.803v14.628h-3.803V26.05h-7.188v5.702h-3.669V17.125h3.669v5.702Zm25.495-5.702h-5.87c-1.626 0-2.615.307-3.364 1.054-.746.745-1.032 1.69-1.032 3.357v5.812c0 1.668.286 2.61 1.032 3.357.746.747 1.735 1.054 3.364 1.054h5.87c1.625 0 2.615-.307 3.364-1.054.749-.744 1.032-1.69 1.032-3.357v-5.812c0-1.668-.286-2.61-1.032-3.357-.749-.747-1.739-1.054-3.364-1.054Zm.724 10.308c0 .857-.286 1.097-1.318 1.097h-4.682c-1.032 0-1.319-.24-1.319-1.097v-5.988c0-.857.286-1.097 1.319-1.097h4.682c1.056 0 1.318.22 1.318 1.097v5.988Zm12.768-4.454h5.252c1.626 0 2.637.307 3.364 1.054.658.66 1.032 1.732 1.032 2.896v.878c0 1.163-.374 2.214-1.032 2.895-.727.747-1.717 1.054-3.364 1.054h-10.022v-3.223h9.407c1.032 0 1.318-.24 1.318-1.097v-.438c0-.856-.286-1.096-1.318-1.096h-5.252c-1.628 0-2.639-.31-3.364-1.054-.657-.66-1.032-1.732-1.032-2.896v-.878c0-1.163.372-2.214 1.032-2.895.728-.747 1.714-1.054 3.364-1.054h9.499v3.223h-8.884c-1.032 0-1.318.24-1.318 1.097v.438c0 .856.286 1.096 1.318 1.096Zm11.974-2.63v-3.224h14.945v3.223h-5.626v11.405h-3.671V20.348h-5.648Z" clip-rule="evenodd"/><path fill="#0052CD" d="M63.532 17.01h-5.87c-1.625 0-2.615.306-3.364 1.054-.746.744-1.032 1.689-1.032 3.354V31.75h3.672V21.332c0-.856.286-1.096 1.318-1.096h4.682c1.057 0 1.319.219 1.319 1.097v10.42h3.671V21.42c0-1.668-.286-2.61-1.032-3.354-.746-.75-1.735-1.057-3.364-1.057Z"/><g clip-path="url(#b)"><path fill="#0052CD" d="M40.811 10.287 24.081.647a4.901 4.901 0 0 0-4.867 0 4.868 4.868 0 0 0-2.433 4.205v1.257l-1.09-.628a4.901 4.901 0 0 0-4.868 0 4.869 4.869 0 0 0-2.432 4.207v1.258l-1.09-.629a4.901 4.901 0 0 0-4.868 0A4.869 4.869 0 0 0 0 14.525v30.203a2.29 2.29 0 0 0 1.29 2.054c.783.38 1.733.276 2.415-.261l8.296-6.53 12.793 7.371a2.29 2.29 0 0 0 1.145.304 2.288 2.288 0 0 0 2.29-2.281V27.207a8.392 8.392 0 0 0-4.196-7.252l-4.195-2.418V4.855a1.813 1.813 0 0 1 2.722-1.568l16.73 9.637a5.342 5.342 0 0 1 2.67 4.614v22.633c0 .644-.348 1.246-.908 1.568l-4.433 2.555V22.372a8.393 8.393 0 0 0-4.195-7.252l-10.3-5.933v3.515l8.775 5.055a5.338 5.338 0 0 1 2.67 4.615v23.237c0 .811.438 1.57 1.144 1.978a2.29 2.29 0 0 0 1.145.304c.396 0 .792-.104 1.145-.304l5.577-3.214a4.87 4.87 0 0 0 2.433-4.208V17.532a8.412 8.412 0 0 0-4.202-7.245Zm-18.31 12.306a5.339 5.339 0 0 1 2.67 4.615v16.86l-10.594-6.102 3.4-2.674a4.82 4.82 0 0 0 1.855-3.813V21.06l2.67 1.534Zm-5.72-3.296v12.176c0 .56-.252 1.079-.69 1.422L3.05 43.155V14.52a1.814 1.814 0 0 1 2.721-1.567l2.619 1.507v21.585l3.05-2.4V9.688a1.813 1.813 0 0 1 2.722-1.567l2.618 1.506v6.152l-3.05-1.759v3.518l3.05 1.76Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h141.873v48H0z"/></clipPath><clipPath id="b"><path fill="#fff" d="M0 0h45.013v47.891H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
1
dashboard/public/assets/grafana.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
dashboard/public/assets/line-grid.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="1003" height="644" fill="none" xmlns="http://www.w3.org/2000/svg"><g opacity=".24"><mask id="b" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="128" y="0" width="921" height="644"><ellipse cx="588.5" cy="322" rx="460.5" ry="322" fill="url(#a)" fill-opacity=".32"/></mask><g mask="url(#b)" stroke="#fff" stroke-opacity=".88"><path d="M141.5 17v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609M-51 33.5h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280"/></g></g><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 322 -460.5 0 588.5 322)"><stop/><stop offset="1" stop-opacity="0"/></radialGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
dashboard/public/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="119" height="40" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)" fill="#fff"><path fill-rule="evenodd" clip-rule="evenodd" d="M63.32 19.023h5.99V14.27h3.17v12.19h-3.17V21.71h-5.99v4.752h-3.058v-12.19h3.057v4.752h.002Zm21.245-4.752h-4.89c-1.356 0-2.18.255-2.804.879-.622.62-.86 1.408-.86 2.798v4.842c0 1.39.237 2.175.86 2.797.62.623 1.446.88 2.803.88h4.891c1.356 0 2.18-.257 2.804-.88.625-.62.86-1.407.86-2.797v-4.842c0-1.39-.237-2.175-.86-2.8-.625-.622-1.448-.877-2.803-.877Zm.604 8.59c0 .713-.237.913-1.099.913h-3.9c-.86 0-1.1-.2-1.1-.913v-4.99c0-.713.24-.913 1.1-.913h3.9c.882 0 1.1.18 1.1.912v4.991Zm10.64-3.712h4.376c1.355 0 2.198.256 2.804.878.549.55.86 1.444.86 2.413v.732c0 .97-.312 1.845-.86 2.413-.606.622-1.431.879-2.804.879h-8.35v-2.688h7.838c.86 0 1.1-.2 1.1-.912v-.365c0-.714-.24-.914-1.1-.914h-4.375c-1.359 0-2.2-.259-2.804-.879-.549-.55-.861-1.442-.861-2.412v-.732c0-.97.31-1.846.861-2.412.606-.624 1.428-.879 2.803-.879h7.916v2.686h-7.404c-.86 0-1.099.2-1.099.913v.365c0 .715.24.915 1.1.915v-.001Zm9.979-2.192V14.27h12.454v2.688h-4.688v9.503h-3.06v-9.503h-4.707.001Z"/><path d="M52.946 14.175h-4.892c-1.355 0-2.18.255-2.803.879-.622.62-.86 1.407-.86 2.795v8.61h3.058v-8.684c0-.712.24-.912 1.1-.912h3.902c.88 0 1.098.182 1.098.912v8.685h3.06v-8.61c0-1.39-.238-2.175-.86-2.795-.622-.625-1.446-.881-2.803-.881v.001ZM34.01 8.573 20.069.538a4.085 4.085 0 0 0-4.058 0 4.057 4.057 0 0 0-2.026 3.503V5.09l-.909-.525a4.085 4.085 0 0 0-4.057 0 4.058 4.058 0 0 0-2.027 3.51v1.048l-.908-.524a4.085 4.085 0 0 0-4.056 0A4.058 4.058 0 0 0 0 12.104v25.17a1.907 1.907 0 0 0 3.087 1.494L10 33.325l10.663 6.143a1.936 1.936 0 0 0 1.907 0c.587-.34.954-.97.954-1.65V22.676a6.995 6.995 0 0 0-3.497-6.044l-3.496-2.015V4.046A1.513 1.513 0 0 1 18.8 2.738l13.941 8.031a4.452 4.452 0 0 1 2.225 3.845v18.861c0 .538-.29 1.038-.756 1.307l-3.694 2.128V18.643A6.994 6.994 0 0 0 27.02 12.6l-8.582-4.944v2.93l7.31 4.212a4.448 4.448 0 0 1 2.226 3.845v19.365c0 .675.365 1.308.953 1.648a1.936 1.936 0 0 0 1.908 0l4.649-2.678a4.058 4.058 0 0 0 2.027-3.507v-18.86a7.01 7.01 0 0 0-3.501-6.038ZM18.75 18.828a4.448 4.448 0 0 1 2.225 3.846v14.05l-8.828-5.086 2.834-2.228a4.018 4.018 0 0 0 1.545-3.177V17.55l2.225 1.279-.001-.001Zm-4.766-2.747v10.147c0 .466-.211.9-.576 1.185L2.54 35.963V12.1a1.512 1.512 0 0 1 2.268-1.306l2.183 1.256v17.988l2.542-2V8.073a1.511 1.511 0 0 1 2.268-1.307l2.182 1.255v5.128l-2.54-1.465v2.931l2.542 1.466h-.002Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h118.75v40H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 140 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="white" fill-opacity="0.15"/>
|
||||
<rect width="200" height="200" fill="#263245" fill-opacity="0.08"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M71 84C71 67.9837 83.9837 55 100 55C116.016 55 129 67.9837 129 84C129 100.016 116.016 113 100 113C83.9837 113 71 100.016 71 84ZM100 49C80.67 49 65 64.67 65 84C65 97.6014 72.7585 109.391 84.0914 115.184C79.3584 116.509 74.7892 118.425 70.496 120.903C61.5257 126.08 54.0757 133.527 48.8946 142.495C48.0657 143.929 48.5568 145.764 49.9914 146.593C51.4261 147.422 53.261 146.931 54.0898 145.496C58.7443 137.44 65.4368 130.75 73.4952 126.099C81.5536 121.448 90.694 119 99.9982 119C109.302 119 118.443 121.449 126.501 126.1C134.559 130.751 141.252 137.441 145.906 145.497C146.735 146.932 148.57 147.423 150.004 146.594C151.439 145.765 151.93 143.93 151.101 142.496C145.92 133.527 138.471 126.081 129.5 120.903C125.208 118.426 120.639 116.509 115.907 115.185C127.241 109.392 135 97.6021 135 84C135 64.67 119.33 49 100 49Z" fill="white" fill-opacity="0.15"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M71 84C71 67.9837 83.9837 55 100 55C116.016 55 129 67.9837 129 84C129 100.016 116.016 113 100 113C83.9837 113 71 100.016 71 84ZM100 49C80.67 49 65 64.67 65 84C65 97.6014 72.7585 109.391 84.0914 115.184C79.3584 116.509 74.7892 118.425 70.496 120.903C61.5257 126.08 54.0757 133.527 48.8946 142.495C48.0657 143.929 48.5568 145.764 49.9914 146.593C51.4261 147.422 53.261 146.931 54.0898 145.496C58.7443 137.44 65.4368 130.75 73.4952 126.099C81.5536 121.448 90.694 119 99.9982 119C109.302 119 118.443 121.449 126.501 126.1C134.559 130.751 141.252 137.441 145.906 145.497C146.735 146.932 148.57 147.423 150.004 146.594C151.439 145.765 151.93 143.93 151.101 142.496C145.92 133.527 138.471 126.081 129.5 120.903C125.208 118.426 120.639 116.509 115.907 115.185C127.241 109.392 135 97.6021 135 84C135 64.67 119.33 49 100 49Z" fill="#263245" fill-opacity="0.25"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -111,9 +111,12 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { deployments } = deploymentPageData || {};
|
||||
const { deployments } = deploymentPageData || { deployments: [] };
|
||||
const { deployments: scheduledOrPendingDeployments } =
|
||||
scheduledOrPendingDeploymentsData || {};
|
||||
scheduledOrPendingDeploymentsData || { deployments: [] };
|
||||
const isDeploymentInProgress = deployments?.some((deployment) =>
|
||||
['PENDING', 'SCHEDULED'].includes(deployment.deploymentStatus),
|
||||
);
|
||||
|
||||
const latestDeployment = latestDeploymentData?.deployments[0];
|
||||
const latestLiveDeployment = latestLiveDeploymentData?.deployments[0];
|
||||
@@ -135,7 +138,10 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
deployment={deployment}
|
||||
isLive={liveDeploymentId === deployment.id}
|
||||
showRedeploy={latestDeployment.id === deployment.id}
|
||||
disableRedeploy={scheduledOrPendingDeployments.length > 0}
|
||||
disableRedeploy={
|
||||
scheduledOrPendingDeployments?.length > 0 ||
|
||||
isDeploymentInProgress
|
||||
}
|
||||
/>
|
||||
|
||||
{index !== deployments.length - 1 && <Divider component="li" />}
|
||||
@@ -143,7 +149,7 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
))}
|
||||
</List>
|
||||
<div className="mt-8 flex w-full justify-center">
|
||||
<div className="grid grid-flow-col gap-2 items-center">
|
||||
<div className="grid grid-flow-col items-center gap-2">
|
||||
<NextPrevPageLink
|
||||
direction="prev"
|
||||
prevAllowed={page !== 1}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import FeedbackForm from '@/components/common/FeedbackForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useInterval } from '@/hooks/useInterval';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -33,7 +33,7 @@ export function AppLoader({
|
||||
date,
|
||||
restoring,
|
||||
}: AppLoaderProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
let timeElapsedSinceEventCreation: number;
|
||||
|
||||
@@ -41,11 +41,11 @@ export function AppLoader({
|
||||
timeElapsedSinceEventCreation = getRelativeDateByApplicationState(date);
|
||||
} else if (unpause) {
|
||||
timeElapsedSinceEventCreation = getRelativeDateByApplicationState(
|
||||
currentApplication.appStates[0].createdAt,
|
||||
currentProject.appStates[0].createdAt,
|
||||
);
|
||||
} else {
|
||||
timeElapsedSinceEventCreation = getRelativeDateByApplicationState(
|
||||
currentApplication.createdAt,
|
||||
currentProject.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,9 +63,9 @@ export function AppLoader({
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h1">
|
||||
{restoring && `Restoring ${currentApplication.name} from backup`}
|
||||
{!restoring && unpause && `Unpausing ${currentApplication.name}`}
|
||||
{!restoring && !unpause && `Provisioning ${currentApplication.name}`}
|
||||
{restoring && `Restoring ${currentProject.name} from backup`}
|
||||
{!restoring && unpause && `Unpausing ${currentProject.name}`}
|
||||
{!restoring && !unpause && `Provisioning ${currentProject.name}`}
|
||||
</Text>
|
||||
<Text>This normally takes around 2 minutes</Text>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import FeedbackForm from '@/components/common/FeedbackForm';
|
||||
import Container from '@/components/layout/Container';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import { useAppCreatedAt } from '@/hooks/useAppCreatedAt';
|
||||
import { useCurrentDate } from '@/hooks/useCurrentDate';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { ApplicationState } from '@/types/application';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
@@ -10,30 +11,30 @@ import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { getPreviousApplicationState } from '@/utils/getPreviousApplicationState';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import {
|
||||
useDeleteApplicationMutation,
|
||||
useGetApplicationStateQuery,
|
||||
useInsertApplicationMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { getPreviousApplicationState } from '@/utils/getPreviousApplicationState';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
import ApplicationLive from './ApplicationLive';
|
||||
import ApplicationUnknown from './ApplicationUnknown';
|
||||
import { RemoveApplicationModal } from './RemoveApplicationModal';
|
||||
import { StagingMetadata } from './StagingMetadata';
|
||||
|
||||
export default function ApplicationErrored() {
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const {
|
||||
currentWorkspace,
|
||||
currentProject,
|
||||
refetch: refetchProject,
|
||||
} = useCurrentWorkspaceAndProject();
|
||||
const [changingApplicationStateLoading, setChangingApplicationStateLoading] =
|
||||
useState(false);
|
||||
|
||||
@@ -44,22 +45,20 @@ export default function ApplicationErrored() {
|
||||
// state, but we want to query again to double-check that we have the latest state
|
||||
// of the application. @GC.
|
||||
const { data, loading, error } = useGetApplicationStateQuery({
|
||||
variables: { appId: currentApplication.id },
|
||||
variables: { appId: currentProject?.id },
|
||||
skip: !currentProject,
|
||||
});
|
||||
|
||||
const [previousState, setPreviousState] = useState<ApplicationStatus | null>(
|
||||
null,
|
||||
);
|
||||
const previousState = data?.app?.appStates
|
||||
? getPreviousApplicationState(data.app.appStates)
|
||||
: null;
|
||||
|
||||
const [showRecreateModal, setShowRecreateModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [insertApp] = useInsertApplicationMutation();
|
||||
const client = useApolloClient();
|
||||
const { currentDate } = useCurrentDate();
|
||||
const user = useUserData();
|
||||
const isOwner = currentWorkspace.members.some(
|
||||
({ userId, type }) => userId === user.id && type === 'owner',
|
||||
);
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
|
||||
const { appCreatedAt } = useAppCreatedAt();
|
||||
|
||||
@@ -70,15 +69,15 @@ export default function ApplicationErrored() {
|
||||
try {
|
||||
await deleteApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
appId: currentProject.id,
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast(`${currentApplication.name} deleted`);
|
||||
triggerToast(`${currentProject?.name} deleted`);
|
||||
} catch (e) {
|
||||
triggerToast(`Error deleting ${currentApplication.name}`);
|
||||
triggerToast(`Error deleting ${currentProject?.name}`);
|
||||
discordAnnounce(
|
||||
`Error deleting app: ${currentApplication.name} (${user.email})`,
|
||||
`Error deleting app: ${currentProject?.name} (${user.email})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -86,19 +85,19 @@ export default function ApplicationErrored() {
|
||||
await insertApp({
|
||||
variables: {
|
||||
app: {
|
||||
name: currentApplication.name,
|
||||
slug: currentApplication.slug,
|
||||
planId: currentApplication.plan.id,
|
||||
name: currentProject.name,
|
||||
slug: currentProject.slug,
|
||||
planId: currentProject.plan.id,
|
||||
workspaceId: currentWorkspace.id,
|
||||
regionId: currentApplication.region.id,
|
||||
regionId: currentProject.region.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
discordAnnounce(`Recreating: ${currentApplication.name} (${user.email})`);
|
||||
triggerToast(`Recreating ${currentApplication.name} `);
|
||||
await updateOwnCache(client);
|
||||
discordAnnounce(`Recreating: ${currentProject?.name} (${user.email})`);
|
||||
triggerToast(`Recreating ${currentProject?.name} `);
|
||||
await refetchProject();
|
||||
} catch (e) {
|
||||
triggerToast(`Error trying to recreate: ${currentApplication.name}`);
|
||||
triggerToast(`Error trying to recreate: ${currentProject?.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,18 +106,18 @@ export default function ApplicationErrored() {
|
||||
try {
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
appId: currentProject?.id,
|
||||
app: {
|
||||
desiredState: ApplicationStatus.Live,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast(`${currentApplication.name} set to awake.`);
|
||||
triggerToast(`${currentProject?.name} set to awake.`);
|
||||
} catch (e) {
|
||||
triggerToast(`Error trying to awake ${currentApplication.name}`);
|
||||
triggerToast(`Error trying to awake ${currentProject?.name}`);
|
||||
discordAnnounce(
|
||||
`Error trying to awake app: ${currentApplication.name} (${user.email})`,
|
||||
`Error trying to awake app: ${currentProject?.name} (${user.email})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -140,20 +139,6 @@ export default function ApplicationErrored() {
|
||||
await recreateApplication();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousAcceptedState = getPreviousApplicationState(
|
||||
data.app.appStates,
|
||||
);
|
||||
setPreviousState(previousAcceptedState);
|
||||
}, [setPreviousState, data, loading, error]);
|
||||
|
||||
if (loading || previousState === null) {
|
||||
return (
|
||||
<Container className="mx-auto mt-12 max-w-sm text-center">
|
||||
@@ -170,19 +155,13 @@ export default function ApplicationErrored() {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (previousState === ApplicationStatus.Live) {
|
||||
return <ApplicationLive />;
|
||||
}
|
||||
|
||||
// For now, if the application errored and the previous state to this error is an UPDATING state, we want to show the dashboard,
|
||||
// it's likely that most services are up and we shouldn't block all functionality. In the future, we're going to have a way to
|
||||
// redeploy the app again, and get to a healthy state. @GC
|
||||
if (previousState === ApplicationStatus.Updating) {
|
||||
return <ApplicationLive />;
|
||||
}
|
||||
|
||||
if (previousState === ApplicationStatus.Empty) {
|
||||
return <ApplicationUnknown />;
|
||||
if (
|
||||
previousState === ApplicationStatus.Updating ||
|
||||
previousState === ApplicationStatus.Empty
|
||||
) {
|
||||
return (
|
||||
<ApplicationLive errorMessage="Error deploying the project most likely due to invalid configuration. Please review your project's configuration and logs for more information." />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -196,8 +175,8 @@ export default function ApplicationErrored() {
|
||||
// which instead of deleting just an application, it deletes and recreates.
|
||||
handler={recreateApplication}
|
||||
close={() => setShowRecreateModal(false)}
|
||||
title={`Recreate project ${currentApplication.name}?`}
|
||||
description={`The project ${currentApplication.name} will be removed and then re-created. All data will be lost and there will be no way to
|
||||
title={`Recreate project ${currentProject.name}?`}
|
||||
description={`The project ${currentProject?.name} will be removed and then re-created. All data will be lost and there will be no way to
|
||||
recover the app once it has been deleted.`}
|
||||
/>
|
||||
</Modal>
|
||||
@@ -208,8 +187,8 @@ export default function ApplicationErrored() {
|
||||
>
|
||||
<RemoveApplicationModal
|
||||
close={() => setShowDeleteModal(false)}
|
||||
title={`Remove project ${currentApplication.name}?`}
|
||||
description={`The project ${currentApplication.name} will be removed. All data will be lost and there will be no way to
|
||||
title={`Remove project ${currentProject.name}?`}
|
||||
description={`The project ${currentProject?.name} will be removed. All data will be lost and there will be no way to
|
||||
recover the app once it has been deleted.`}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
import { useDeleteApplicationMutation } from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useDeleteApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import getServerError from '@/utils/settings/getServerError';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { useRouter } from 'next/router';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export default function ApplicationInfo() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [deleteApplication, { client }] = useDeleteApplicationMutation();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [deleteApplication] = useDeleteApplicationMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
async function handleClickRemove() {
|
||||
await deleteApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
},
|
||||
});
|
||||
await router.push('/');
|
||||
await client.refetchQueries({
|
||||
include: ['getOneUser'],
|
||||
});
|
||||
triggerToast(`${currentApplication.name} deleted`);
|
||||
try {
|
||||
await toast.promise(
|
||||
deleteApplication({ variables: { appId: currentProject.id } }),
|
||||
{
|
||||
loading: 'Deleting project...',
|
||||
success: 'The project has been deleted successfully.',
|
||||
error: getServerError(
|
||||
'An error occurred while deleting the project. Please try again.',
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
await router.push('/');
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentProject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -35,10 +53,10 @@ export default function ApplicationInfo() {
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() => copy(currentApplication.id, 'Application ID')}
|
||||
onClick={() => copy(currentProject.id, 'Application ID')}
|
||||
className="py-1 text-xs"
|
||||
>
|
||||
{currentApplication.id}
|
||||
{currentProject.id}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -49,27 +67,27 @@ export default function ApplicationInfo() {
|
||||
variant="borderless"
|
||||
onClick={() =>
|
||||
copy(
|
||||
currentApplication.desiredState.toString(),
|
||||
currentProject.desiredState.toString(),
|
||||
'Application Desired State',
|
||||
)
|
||||
}
|
||||
className="py-1 text-xs"
|
||||
>
|
||||
{getApplicationStatusString(currentApplication.desiredState)}
|
||||
{getApplicationStatusString(currentProject.desiredState)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-0.5">
|
||||
<Text variant="subtitle2">Region:</Text>
|
||||
|
||||
<Text variant="subtitle1">{currentApplication.region.city}</Text>
|
||||
<Text variant="subtitle1">{currentProject.region.city}</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-0.5">
|
||||
<Text variant="subtitle2">Created:</Text>
|
||||
|
||||
<Text variant="subtitle1">
|
||||
{formatDistance(new Date(currentApplication.createdAt), new Date(), {
|
||||
{formatDistance(new Date(currentProject.createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</Text>
|
||||
@@ -77,7 +95,7 @@ export default function ApplicationInfo() {
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Link
|
||||
href={`https://staging.nhost.run/console/data/default/schema/public/tables/app_state_history/browse?filter=app_id%3B%24eq%3B${currentApplication.id}`}
|
||||
href={`https://staging.nhost.run/console/data/default/schema/public/tables/app_state_history/browse?filter=app_id%3B%24eq%3B${currentProject.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="grid grid-flow-col items-center justify-center gap-1 p-2"
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import MaintenanceAlert from '@/components/common/MaintenanceAlert';
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import Container from '@/components/layout/Container';
|
||||
import { features } from '@/components/overview/features';
|
||||
import { frameworks } from '@/components/overview/frameworks';
|
||||
import OverviewDeployments from '@/components/overview/OverviewDeployments';
|
||||
import OverviewDocumentation from '@/components/overview/OverviewDocumentation';
|
||||
import OverviewMigration from '@/components/overview/OverviewMigration';
|
||||
import OverviewMetrics from '@/components/overview/OverviewMetrics/OverviewMetrics';
|
||||
import OverviewProjectInfo from '@/components/overview/OverviewProjectInfo';
|
||||
import OverviewRepository from '@/components/overview/OverviewRepository';
|
||||
import OverviewTopBar from '@/components/overview/OverviewTopBar';
|
||||
import OverviewUsage from '@/components/overview/OverviewUsage';
|
||||
import { features } from '@/components/overview/features';
|
||||
import { frameworks } from '@/components/overview/frameworks';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
|
||||
export default function ApplicationLive() {
|
||||
export interface ApplicationLiveProps {
|
||||
/**
|
||||
* Error message to display in the alert.
|
||||
*/
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export default function ApplicationLive({
|
||||
errorMessage,
|
||||
}: ApplicationLiveProps) {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const isProjectUsingRDS = currentApplication?.featureFlags.some(
|
||||
(feature) => feature.name === 'fleetcontrol_use_rds',
|
||||
);
|
||||
|
||||
if (!isPlatform) {
|
||||
return (
|
||||
<Container>
|
||||
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
|
||||
|
||||
<OverviewTopBar />
|
||||
|
||||
<div className="grid grid-cols-1 gap-12 lg:grid-cols-3">
|
||||
@@ -51,38 +60,58 @@ export default function ApplicationLive() {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<MaintenanceAlert />
|
||||
|
||||
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
|
||||
|
||||
<OverviewTopBar />
|
||||
|
||||
<div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3">
|
||||
<div className="order-2 grid grid-flow-row gap-12 lg:order-1 lg:col-span-2">
|
||||
<OverviewDeployments />
|
||||
<div className="grid grid-flow-row gap-12 lg:col-span-2">
|
||||
<RetryableErrorBoundary>
|
||||
<OverviewMetrics />
|
||||
</RetryableErrorBoundary>
|
||||
|
||||
<RetryableErrorBoundary>
|
||||
<OverviewDeployments />
|
||||
</RetryableErrorBoundary>
|
||||
|
||||
<OverviewDocumentation
|
||||
title="Pick your favorite framework and start learning"
|
||||
description="Nhost integrates smoothly with all of the frameworks you already know."
|
||||
cardElements={frameworks}
|
||||
className="hidden lg:block"
|
||||
/>
|
||||
|
||||
<OverviewDocumentation
|
||||
title="Platform Documentation"
|
||||
description="More in-depth documentation for key features."
|
||||
cardElements={features}
|
||||
className="hidden lg:block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="order-1 grid grid-flow-row content-start gap-8 lg:order-2 lg:col-span-1 lg:gap-12">
|
||||
{isProjectUsingRDS && (
|
||||
<>
|
||||
<OverviewMigration />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<div className="grid grid-flow-row content-start gap-8 lg:col-span-1 lg:gap-12">
|
||||
<OverviewProjectInfo />
|
||||
<Divider />
|
||||
<OverviewRepository />
|
||||
<Divider />
|
||||
<OverviewUsage />
|
||||
</div>
|
||||
|
||||
<OverviewDocumentation
|
||||
title="Pick your favorite framework and start learning"
|
||||
description="Nhost integrates smoothly with all of the frameworks you already know."
|
||||
cardElements={frameworks}
|
||||
className="lg:hidden"
|
||||
/>
|
||||
|
||||
<OverviewDocumentation
|
||||
title="Platform Documentation"
|
||||
description="More in-depth documentation for key features."
|
||||
cardElements={features}
|
||||
className="lg:hidden"
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Container from '@/components/layout/Container';
|
||||
import ProjectStatusInfo from '@/components/project/ProjectStatusInfo';
|
||||
import useProjectRedirectWhenReady from '@/hooks/common/useProjectRedirectWhenReady';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useInterval } from '@/hooks/useInterval';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
useInsertFeatureFlagMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserEmail } from '@nhost/nextjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Number of minutes to wait before enabling the "Cancel Migration" button.
|
||||
*/
|
||||
const MIGRATION_CANCEL_TIMEOUT_MINUTES = 15;
|
||||
|
||||
function MigrationDialog() {
|
||||
const { closeAlertDialog } = useDialog();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [countdownTimer, setCountdownTimer] = useState(-1);
|
||||
|
||||
const minutes = Math.floor(countdownTimer / 60);
|
||||
const seconds = Math.floor(countdownTimer % 60);
|
||||
|
||||
const countdownActive = countdownTimer > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawTimestamp = localStorage.getItem(
|
||||
`migration-${currentApplication?.id}`,
|
||||
);
|
||||
|
||||
if (!rawTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = new Date(rawTimestamp);
|
||||
const timeDifference =
|
||||
timestamp.getTime() +
|
||||
1000 * 60 * MIGRATION_CANCEL_TIMEOUT_MINUTES -
|
||||
Date.now();
|
||||
|
||||
if (timeDifference < 0) {
|
||||
setCountdownTimer(0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setCountdownTimer(timeDifference / 1000);
|
||||
}, [currentApplication?.id]);
|
||||
|
||||
useInterval(
|
||||
() =>
|
||||
setCountdownTimer((prev) => {
|
||||
if (prev === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return prev - 1;
|
||||
}),
|
||||
1000,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (countdownTimer !== 0 || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem(`migration-${currentApplication.id}`);
|
||||
}, [countdownTimer, currentApplication.id]);
|
||||
|
||||
const [updateApplication] = useUpdateApplicationMutation({
|
||||
refetchQueries: ['getOneUser'],
|
||||
});
|
||||
const [insertFeatureFlag] = useInsertFeatureFlagMutation();
|
||||
const userEmail = useUserEmail();
|
||||
|
||||
async function handleCancelMigration() {
|
||||
try {
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
desiredState: ApplicationStatus.Live,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await insertFeatureFlag({
|
||||
variables: {
|
||||
flag: {
|
||||
appId: currentApplication.id,
|
||||
name: 'fleetcontrol_use_rds',
|
||||
value: 'console',
|
||||
description: 'Use RDS',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast(`${currentApplication.name} migration cancelled.`);
|
||||
} catch (e) {
|
||||
triggerToast(`Error trying to migrate ${currentApplication.name}`);
|
||||
await discordAnnounce(
|
||||
`Error trying to migrate app: ${currentApplication.subdomain} (${userEmail})`,
|
||||
);
|
||||
} finally {
|
||||
closeAlertDialog();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2 px-6">
|
||||
<Text>
|
||||
Cancelling this migration will revert your project to use the shared
|
||||
Postgres instance.
|
||||
</Text>
|
||||
|
||||
{!countdownActive && (
|
||||
<Alert severity="warning" className="px-3 text-left">
|
||||
Reach out to us at{' '}
|
||||
<Link
|
||||
underline="none"
|
||||
target="_blank"
|
||||
className="hover:underline focus:underline focus:outline-none"
|
||||
href="https://discord.com/channels/552499021260914688/1029043079946182676"
|
||||
>
|
||||
#migratedb
|
||||
</Link>{' '}
|
||||
if you think the migration should have finished by now.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-row gap-2 pb-1">
|
||||
<Button onClick={closeAlertDialog}>Continue Migration</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleCancelMigration}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
disabled={countdownActive}
|
||||
>
|
||||
{countdownActive
|
||||
? `Cancel in ${String(minutes).padStart(2, '0')}:${String(
|
||||
seconds,
|
||||
).padStart(2, '0')}`
|
||||
: 'Cancel Migration'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ApplicationMigrating() {
|
||||
const { openAlertDialog } = useDialog();
|
||||
|
||||
useProjectRedirectWhenReady({ pollInterval: 10000 });
|
||||
|
||||
return (
|
||||
<Container className="flex flex-col gap-6">
|
||||
<ProjectStatusInfo
|
||||
className="mx-auto max-w-sm"
|
||||
title="Migration in progress"
|
||||
description="Your project is being migrated to use a dedicated and more performant Postgres instance."
|
||||
imageProps={{
|
||||
src: '/assets/migrating.svg',
|
||||
alt: 'Application Migrating',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="error"
|
||||
className="mx-auto"
|
||||
onClick={() =>
|
||||
openAlertDialog({
|
||||
title: 'Cancel Migration',
|
||||
payload: <MigrationDialog />,
|
||||
props: {
|
||||
titleProps: {
|
||||
className: 'px-6',
|
||||
},
|
||||
PaperProps: {
|
||||
className: 'py-6 px-0 max-w-sm w-full',
|
||||
},
|
||||
hidePrimaryAction: true,
|
||||
hideSecondaryAction: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Cancel Migration
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -3,54 +3,84 @@ import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
|
||||
import { StagingMetadata } from '@/components/applications/StagingMetadata';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Container from '@/components/layout/Container';
|
||||
import { useUpdateApplicationMutation } from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useGetFreeAndActiveProjectsQuery,
|
||||
useUnpauseApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { Modal } from '@/ui';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { MAX_FREE_PROJECTS } from '@/utils/CONSTANTS';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import type { ApolloError } from '@apollo/client';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { RemoveApplicationModal } from './RemoveApplicationModal';
|
||||
|
||||
export default function ApplicationPaused() {
|
||||
const { openAlertDialog } = useDialog();
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const [changingApplicationStateLoading, setChangingApplicationStateLoading] =
|
||||
useState(false);
|
||||
const [updateApplication, { client }] = useUpdateApplicationMutation();
|
||||
const { id, email } = useUserData();
|
||||
const isOwner = currentWorkspace.members.some(
|
||||
({ userId, type }) => userId === id && type === 'owner',
|
||||
);
|
||||
const isPro = currentApplication.plan.name === 'Pro';
|
||||
const { openDialog } = useDialog();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
const user = useUserData();
|
||||
|
||||
const [showDeletingModal, setShowDeletingModal] = useState(false);
|
||||
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
|
||||
useUnpauseApplicationMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
|
||||
const { data, loading } = useGetFreeAndActiveProjectsQuery({
|
||||
variables: { userId: user?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const numberOfFreeAndLiveProjects = data?.freeAndActiveProjects.length || 0;
|
||||
const wakeUpDisabled = numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS;
|
||||
|
||||
async function handleTriggerUnpausing() {
|
||||
setChangingApplicationStateLoading(true);
|
||||
try {
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
desiredState: ApplicationStatus.Live,
|
||||
await toast.promise(
|
||||
unpauseApplication({ variables: { appId: currentProject.id } }),
|
||||
{
|
||||
loading: 'Starting the project...',
|
||||
success: `The project has been started successfully.`,
|
||||
error: (arg: ApolloError) => {
|
||||
// we need to get the internal error message from the GraphQL error
|
||||
const { internal } = arg.graphQLErrors[0]?.extensions || {};
|
||||
const { message } = (internal as Record<string, any>)?.error || {};
|
||||
|
||||
// we use the default Apollo error message if we can't find the
|
||||
// internal error message
|
||||
return (
|
||||
message ||
|
||||
arg.message ||
|
||||
'An error occurred while waking up the project. Please try again.'
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
await updateOwnCache(client);
|
||||
discordAnnounce(
|
||||
`App ${currentApplication.name} (${email}) set to awake.`,
|
||||
getToastStyleProps(),
|
||||
);
|
||||
triggerToast(`${currentApplication.name} set to awake.`);
|
||||
} catch (e) {
|
||||
triggerToast(`Error trying to awake ${currentApplication.name}`);
|
||||
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator label="Loading user data..." delay={1000} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@@ -59,13 +89,13 @@ export default function ApplicationPaused() {
|
||||
>
|
||||
<RemoveApplicationModal
|
||||
close={() => setShowDeletingModal(false)}
|
||||
title={`Remove project ${currentApplication.name}?`}
|
||||
description={`The project ${currentApplication.name} will be removed. All data will be lost and there will be no way to
|
||||
title={`Remove project ${currentProject.name}?`}
|
||||
description={`The project ${currentProject.name} will be removed. All data will be lost and there will be no way to
|
||||
recover the app once it has been deleted.`}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Container className="mx-auto mt-20 grid max-w-sm grid-flow-row gap-2 text-center">
|
||||
<Container className="mx-auto mt-20 grid max-w-lg grid-flow-row gap-4 text-center">
|
||||
<div className="mx-auto flex w-centImage flex-col text-center">
|
||||
<Image
|
||||
src="/assets/PausedApp.svg"
|
||||
@@ -75,58 +105,67 @@ export default function ApplicationPaused() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text variant="h3" component="h1" className="mt-4">
|
||||
{currentApplication.name} is sleeping
|
||||
</Text>
|
||||
<Box className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h1">
|
||||
{currentProject.name} is sleeping
|
||||
</Text>
|
||||
|
||||
<Text className="mt-1">
|
||||
Projects on the free plan stop responding to API calls after 7 days of
|
||||
no traffic.
|
||||
</Text>
|
||||
|
||||
{!isPro && (
|
||||
<Button
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
onClick={() => {
|
||||
openAlertDialog({
|
||||
title: 'Upgrade your plan.',
|
||||
payload: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0' },
|
||||
hidePrimaryAction: true,
|
||||
hideSecondaryAction: true,
|
||||
hideTitle: true,
|
||||
maxWidth: 'lg',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade to Pro to avoid autosleep
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
loading={changingApplicationStateLoading}
|
||||
disabled={changingApplicationStateLoading}
|
||||
onClick={handleTriggerUnpausing}
|
||||
>
|
||||
Wake Up
|
||||
</Button>
|
||||
<Text>
|
||||
Starter projects stop responding to API calls after 7 days of
|
||||
inactivity. Upgrade to Pro to avoid autosleep.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-row gap-2">
|
||||
{isOwner && (
|
||||
<Button
|
||||
color="error"
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
component: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0' },
|
||||
maxWidth: 'lg',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete Project
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
loading={changingApplicationStateLoading}
|
||||
disabled={changingApplicationStateLoading || wakeUpDisabled}
|
||||
onClick={handleTriggerUnpausing}
|
||||
>
|
||||
Wake Up
|
||||
</Button>
|
||||
|
||||
{wakeUpDisabled && (
|
||||
<Alert severity="warning" className="mx-auto max-w-xs text-left">
|
||||
Note: Only one free project can be active at any given time.
|
||||
Please pause your active free project before unpausing{' '}
|
||||
{currentProject.name}.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
color="error"
|
||||
variant="borderless"
|
||||
className="mx-auto w-full max-w-[280px]"
|
||||
onClick={() => setShowDeletingModal(true)}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<StagingMetadata>
|
||||
<ApplicationInfo />
|
||||
</StagingMetadata>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import Container from '@/components/layout/Container';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useCheckProvisioning } from '@/hooks/useCheckProvisioning';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import Image from 'next/image';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
import { AppLoader } from './AppLoader';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
import { StagingMetadata } from './StagingMetadata';
|
||||
|
||||
export default function ApplicationProvisioning() {
|
||||
const currentApplicationState = useCheckProvisioning();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const currentProjectState = useCheckProvisioning();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
return (
|
||||
<Container className="mx-auto mt-8 grid max-w-sm grid-flow-row gap-4 text-center">
|
||||
@@ -24,16 +24,16 @@ export default function ApplicationProvisioning() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentApplicationState.state === ApplicationStatus.Empty ? (
|
||||
{currentProjectState.state === ApplicationStatus.Empty ? (
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h1">
|
||||
Setting Up {currentApplication.name}
|
||||
Setting Up {currentProject?.name}
|
||||
</Text>
|
||||
<Text>This normally takes around 2 minutes</Text>
|
||||
<ActivityIndicator className="mx-auto" />
|
||||
</div>
|
||||
) : (
|
||||
<AppLoader startLoader date={currentApplicationState.createdAt} />
|
||||
<AppLoader startLoader date={currentProjectState.createdAt} />
|
||||
)}
|
||||
|
||||
<StagingMetadata>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import Container from '@/components/layout/Container';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useCheckProvisioning } from '@/hooks/useCheckProvisioning';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import Image from 'next/image';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
import { AppLoader } from './AppLoader';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
import { StagingMetadata } from './StagingMetadata';
|
||||
|
||||
export default function ApplicationRestoring() {
|
||||
const currentApplicationState = useCheckProvisioning();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const currentProjectState = useCheckProvisioning();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
return (
|
||||
<Container className="mx-auto mt-8 grid max-w-sm grid-flow-row gap-4 text-center">
|
||||
@@ -23,10 +23,10 @@ export default function ApplicationRestoring() {
|
||||
height={72}
|
||||
/>
|
||||
</div>
|
||||
{currentApplicationState.state === ApplicationStatus.Empty ? (
|
||||
{currentProjectState.state === ApplicationStatus.Empty ? (
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h1">
|
||||
Setting Up {currentApplication.name}
|
||||
Setting Up {currentProject?.name}
|
||||
</Text>
|
||||
|
||||
<Text>This normally takes around 2 minutes</Text>
|
||||
@@ -34,11 +34,7 @@ export default function ApplicationRestoring() {
|
||||
<ActivityIndicator className="mx-auto" />
|
||||
</div>
|
||||
) : (
|
||||
<AppLoader
|
||||
startLoader
|
||||
restoring
|
||||
date={currentApplicationState.createdAt}
|
||||
/>
|
||||
<AppLoader startLoader restoring date={currentProjectState.createdAt} />
|
||||
)}
|
||||
<StagingMetadata>
|
||||
<ApplicationInfo />
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import FeedbackForm from '@/components/common/FeedbackForm';
|
||||
import Container from '@/components/layout/Container';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
|
||||
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/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
@@ -13,13 +13,9 @@ import { RemoveApplicationModal } from './RemoveApplicationModal';
|
||||
import { StagingMetadata } from './StagingMetadata';
|
||||
|
||||
export default function ApplicationUnknown() {
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const user = useUserData();
|
||||
const isOwner = currentWorkspace.members.some(
|
||||
({ userId, type }) => userId === user.id && type === 'owner',
|
||||
);
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -29,8 +25,8 @@ export default function ApplicationUnknown() {
|
||||
>
|
||||
<RemoveApplicationModal
|
||||
close={() => setShowDeleteModal(false)}
|
||||
title={`Remove project ${currentApplication.name}?`}
|
||||
description={`The project ${currentApplication.name} will be removed. All data will be lost and there will be no way to
|
||||
title={`Remove project ${currentProject.name}?`}
|
||||
description={`The project ${currentProject.name} will be removed. All data will be lost and there will be no way to
|
||||
recover the app once it has been deleted.`}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Container from '@/components/layout/Container';
|
||||
import useProjectRedirectWhenReady from '@/hooks/common/useProjectRedirectWhenReady';
|
||||
import { useProjectRedirectWhenReady } from '@/features/projects/common/hooks/useProjectRedirectWhenReady';
|
||||
import Image from 'next/image';
|
||||
import { AppLoader } from './AppLoader';
|
||||
|
||||
|
||||
@@ -1,43 +1,39 @@
|
||||
import { BillingPaymentMethodForm } from '@/components/billing-payment-method/BillingPaymentMethodForm';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/context/UIContext';
|
||||
import { BillingPaymentMethodForm } from '@/components/workspace/BillingPaymentMethodForm';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
refetchGetApplicationPlanQuery,
|
||||
useGetAppPlanAndGlobalPlansQuery,
|
||||
useGetPaymentMethodsQuery,
|
||||
useUpdateAppMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import useApplicationState from '@/hooks/useApplicationState';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Checkbox from '@/ui/v2/Checkbox';
|
||||
import { BaseDialog } from '@/ui/v2/Dialog';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { planDescriptions } from '@/utils/planDescriptions';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useTheme } from '@mui/material';
|
||||
import getServerError from '@/utils/settings/getServerError';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
function Plan({
|
||||
planName,
|
||||
price,
|
||||
setPlan,
|
||||
planId,
|
||||
selectedPlanId,
|
||||
currentPlan,
|
||||
}: any) {
|
||||
function Plan({ planName, price, setPlan, planId, selectedPlanId }: any) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="my-4 grid w-full grid-flow-col items-center justify-between px-1"
|
||||
className="my-4 grid w-full grid-flow-col items-center justify-between gap-2 px-1"
|
||||
onClick={setPlan}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="grid grid-flow-row gap-y-0.5">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="grid grid-flow-col items-center justify-start gap-2">
|
||||
<Checkbox
|
||||
onChange={setPlan}
|
||||
checked={selectedPlanId === planId}
|
||||
@@ -47,78 +43,107 @@ function Plan({
|
||||
<Text
|
||||
variant="h3"
|
||||
component="p"
|
||||
className="ml-2 self-center font-medium"
|
||||
className="self-center text-left font-medium"
|
||||
>
|
||||
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
|
||||
Upgrade to {planName}
|
||||
</Text>
|
||||
</div>
|
||||
<Text variant="subtitle2" className="w-64 text-start">
|
||||
|
||||
<Text variant="subtitle2" className="w-full max-w-[256px] text-start">
|
||||
{planDescriptions[planName]}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text variant="h3" component="p">
|
||||
$ {price}/mo
|
||||
${price}/mo
|
||||
</Text>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
const theme = useTheme();
|
||||
const [selectedPlanId, setSelectedPlanId] = useState('');
|
||||
const { closeAlertDialog } = useDialog();
|
||||
const [pollingCurrentProject, setPollingCurrentProject] = useState(false);
|
||||
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const {
|
||||
currentWorkspace,
|
||||
currentProject,
|
||||
refetch: refetchWorkspaceAndProject,
|
||||
} = useCurrentWorkspaceAndProject();
|
||||
const { state } = useApplicationState();
|
||||
|
||||
// get workspace payment methods
|
||||
const { data } = useGetPaymentMethodsQuery({
|
||||
variables: {
|
||||
workspaceId: currentWorkspace.id,
|
||||
workspaceId: currentWorkspace?.id,
|
||||
},
|
||||
skip: !currentWorkspace,
|
||||
});
|
||||
|
||||
const { openPaymentModal, closePaymentModal, paymentModal } = useUI();
|
||||
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
||||
const paymentMethodAvailable = data?.paymentMethods.length > 0;
|
||||
|
||||
const currentPlan = plans.find((plan) => plan.id === app.plan.id);
|
||||
const selectedPlan = plans.find((plan) => plan.id === selectedPlanId);
|
||||
|
||||
const isDowngrade = currentPlan.price > selectedPlan?.price;
|
||||
useEffect(() => {
|
||||
if (!pollingCurrentProject || state === ApplicationStatus.Paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
// graphql mutations
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
close?.();
|
||||
closeAlertDialog();
|
||||
setShowPaymentModal(false);
|
||||
setPollingCurrentProject(false);
|
||||
}, [state, pollingCurrentProject, close, closeAlertDialog]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pollingCurrentProject) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refetchWorkspaceAndProject();
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [pollingCurrentProject, refetchWorkspaceAndProject, currentProject]);
|
||||
|
||||
const [updateApp] = useUpdateApplicationMutation({
|
||||
refetchQueries: [
|
||||
refetchGetApplicationPlanQuery({
|
||||
workspace: currentWorkspace.slug,
|
||||
slug: currentApplication.slug,
|
||||
slug: currentProject.slug,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// function handlers
|
||||
const handleUpdateAppPlan = async () => {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: app.id,
|
||||
app: {
|
||||
planId: selectedPlan.id,
|
||||
try {
|
||||
await toast.promise(
|
||||
updateApp({
|
||||
variables: {
|
||||
appId: app.id,
|
||||
app: {
|
||||
planId: selectedPlan.id,
|
||||
desiredState: 5,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
loading: 'Updating plan...',
|
||||
success: `Plan has been updated successfully to ${selectedPlan.name}.`,
|
||||
error: getServerError(
|
||||
'An error occurred while updating the plan. Please try again.',
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
if (isDowngrade) {
|
||||
if (close) {
|
||||
close();
|
||||
}
|
||||
|
||||
closeAlertDialog();
|
||||
setPollingCurrentProject(true);
|
||||
} catch (error) {
|
||||
// Note: Error is handled by the toast.
|
||||
}
|
||||
|
||||
triggerToast(
|
||||
`${currentApplication.name} plan changed to ${selectedPlan.name}.`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleChangePlanClick = async () => {
|
||||
@@ -127,33 +152,114 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
}
|
||||
|
||||
if (!paymentMethodAvailable) {
|
||||
openPaymentModal();
|
||||
setShowPaymentModal(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await handleUpdateAppPlan();
|
||||
|
||||
if (close) {
|
||||
close();
|
||||
}
|
||||
|
||||
closeAlertDialog();
|
||||
};
|
||||
|
||||
if (pollingCurrentProject) {
|
||||
return (
|
||||
<Box className="mx-auto w-full max-w-xl rounded-lg p-6 text-left">
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto">
|
||||
<Image
|
||||
src="/assets/upgrade.svg"
|
||||
alt="Nhost Logo"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text variant="h3" component="h2" className="mt-2 text-center">
|
||||
Successfully upgraded to {currentPlan.name}
|
||||
</Text>
|
||||
|
||||
<ActivityIndicator
|
||||
label="We are unpausing your project. This may take some time..."
|
||||
className="mx-auto mt-2"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="mx-auto mt-4 w-full max-w-sm"
|
||||
onClick={() => {
|
||||
if (close) {
|
||||
close();
|
||||
}
|
||||
|
||||
closeAlertDialog();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (app.plan.id !== plans.find((plan) => plan.isFree)?.id) {
|
||||
return (
|
||||
<Box className="mx-auto w-full max-w-xl rounded-lg p-6 text-left">
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto">
|
||||
<Image
|
||||
src="/assets/upgrade.svg"
|
||||
alt="Nhost Logo"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
</div>
|
||||
<Text variant="h3" component="h2" className="mt-2 text-center">
|
||||
Downgrade is not available
|
||||
</Text>
|
||||
|
||||
<Text className="mt-1 text-center">
|
||||
You can't downgrade from a paid plan to a free plan here.
|
||||
</Text>
|
||||
|
||||
<Text className="text-center">
|
||||
Please contact us at{' '}
|
||||
<Link href="mailto:info@nhost.io">info@nhost.io</Link> if you want
|
||||
to downgrade.
|
||||
</Text>
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="mx-auto w-full max-w-sm"
|
||||
onClick={() => {
|
||||
if (close) {
|
||||
close();
|
||||
}
|
||||
|
||||
closeAlertDialog();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="w-welcome rounded-lg p-6 text-left">
|
||||
<Modal
|
||||
showModal={paymentModal}
|
||||
close={closePaymentModal}
|
||||
dialogStyle={{ zIndex: theme.zIndex.modal + 1 }}
|
||||
<Box className="w-full max-w-xl rounded-lg p-6 text-left">
|
||||
<BaseDialog
|
||||
open={showPaymentModal}
|
||||
onClose={() => setShowPaymentModal(false)}
|
||||
>
|
||||
<BillingPaymentMethodForm
|
||||
close={closePaymentModal}
|
||||
onPaymentMethodAdded={handleUpdateAppPlan}
|
||||
workspaceId={currentWorkspace.id}
|
||||
/>
|
||||
</Modal>
|
||||
</BaseDialog>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto">
|
||||
<Image
|
||||
@@ -170,7 +276,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
You're currently on the <strong>{app.plan.name}</strong> plan.
|
||||
</Text>
|
||||
|
||||
<div className="mt-5">
|
||||
<div className="mt-2">
|
||||
{plans
|
||||
.filter((plan) => plan.id !== app.plan.id)
|
||||
.map((plan) => (
|
||||
@@ -188,11 +294,13 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Button onClick={handleChangePlanClick} disabled={!selectedPlan}>
|
||||
{!selectedPlan && 'Change Plan'}
|
||||
{selectedPlan && isDowngrade && 'Downgrade'}
|
||||
{selectedPlan && !isDowngrade && 'Upgrade'}
|
||||
<div className="mt-2 grid grid-flow-row gap-2">
|
||||
<Button
|
||||
onClick={handleChangePlanClick}
|
||||
disabled={!selectedPlan}
|
||||
loading={pollingCurrentProject}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -216,14 +324,12 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
|
||||
export interface ChangePlanModalProps {
|
||||
/**
|
||||
* Function to close the modal if mounted on parent component.
|
||||
*
|
||||
* @deprecated Implement modal by using `openAlertDialog` hook instead.
|
||||
* Function to close the modal.
|
||||
*/
|
||||
close?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export function ChangePlanModal({ close }: ChangePlanModalProps) {
|
||||
export function ChangePlanModal({ onCancel }: ChangePlanModalProps) {
|
||||
const {
|
||||
query: { workspaceSlug, appSlug },
|
||||
} = useRouter();
|
||||
@@ -249,5 +355,5 @@ export function ChangePlanModal({ close }: ChangePlanModalProps) {
|
||||
const { apps, plans } = data;
|
||||
const app = apps[0];
|
||||
|
||||
return <ChangePlanModalWithData app={app} plans={plans} close={close} />;
|
||||
return <ChangePlanModalWithData app={app} plans={plans} close={onCancel} />;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { LoadingScreen } from '@/components/common/LoadingScreen';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import CopyIcon from '@/ui/v2/icons/CopyIcon';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import generateAppServiceUrl, {
|
||||
defaultLocalBackendSlugs,
|
||||
defaultRemoteBackendSlugs,
|
||||
} from '@/utils/common/generateAppServiceUrl';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||
import { getHasuraConsoleServiceUrl } from '@/utils/env';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface HasuraDataProps {
|
||||
@@ -20,22 +20,20 @@ interface HasuraDataProps {
|
||||
}
|
||||
|
||||
export function HasuraData({ close }: HasuraDataProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const projectAdminSecret = currentProject?.config?.hasura.adminSecret;
|
||||
|
||||
if (
|
||||
!currentApplication?.subdomain ||
|
||||
!currentApplication?.hasuraGraphqlAdminSecret
|
||||
) {
|
||||
if (!currentProject?.subdomain || !projectAdminSecret) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||
? `${LOCAL_HASURA_URL}/console`
|
||||
? `${getHasuraConsoleServiceUrl()}`
|
||||
: generateAppServiceUrl(
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region.awsName,
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
'hasura',
|
||||
defaultLocalBackendSlugs,
|
||||
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
|
||||
@@ -71,18 +69,11 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
|
||||
<div className="col-span-1 grid grid-flow-col items-center justify-center gap-2 sm:col-span-2 sm:justify-end">
|
||||
<Text className="font-medium" variant="subtitle2">
|
||||
{Array(currentApplication.hasuraGraphqlAdminSecret.length)
|
||||
.fill('•')
|
||||
.join('')}
|
||||
{Array(projectAdminSecret.length).fill('•').join('')}
|
||||
</Text>
|
||||
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
copy(
|
||||
currentApplication.hasuraGraphqlAdminSecret,
|
||||
'Hasura admin secret',
|
||||
)
|
||||
}
|
||||
onClick={() => copy(projectAdminSecret, 'Hasura admin secret')}
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="min-w-0 p-1"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Checkbox from '@/ui/v2/Checkbox';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useDeleteApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
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';
|
||||
@@ -42,13 +45,15 @@ export function RemoveApplicationModal({
|
||||
description,
|
||||
className,
|
||||
}: RemoveApplicationModalProps) {
|
||||
const [deleteApplication, { client }] = useDeleteApplicationMutation();
|
||||
const [deleteApplication] = useDeleteApplicationMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [remove2, setRemove2] = useState(false);
|
||||
const appName = currentApplication?.name;
|
||||
const appName = currentProject?.name;
|
||||
|
||||
async function handleClick() {
|
||||
setLoadingRemove(true);
|
||||
@@ -65,7 +70,7 @@ export function RemoveApplicationModal({
|
||||
try {
|
||||
await deleteApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
appId: currentProject.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -73,10 +78,7 @@ export function RemoveApplicationModal({
|
||||
}
|
||||
close();
|
||||
await router.push('/');
|
||||
await client.refetchQueries({
|
||||
include: ['getOneUser'],
|
||||
});
|
||||
triggerToast(`${currentApplication.name} deleted`);
|
||||
triggerToast(`${currentProject.name} deleted`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
import { FindOldApps } from '@/components/home';
|
||||
import type { UserData } from '@/hooks/useGetAllUserWorkspacesAndApplications';
|
||||
import type { Application, ApplicationState } from '@/types/application';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import StateBadge from '@/ui/StateBadge';
|
||||
import type { DeploymentStatus } from '@/ui/StatusCircle';
|
||||
import { StatusCircle } from '@/ui/StatusCircle';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import Image from 'next/image';
|
||||
import NavLink from 'next/link';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
function ApplicationCreatedAt({ createdAt }: any) {
|
||||
return (
|
||||
<Text component="span" className="text-sm">
|
||||
created{' '}
|
||||
{formatDistance(new Date(createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
function LastSuccessfulDeployment({ deployment }: any) {
|
||||
return (
|
||||
<span className="flex flex-row">
|
||||
<Avatar
|
||||
component="span"
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
className="mr-1 h-4 w-4 self-center"
|
||||
/>
|
||||
<Text component="span" className="self-center text-sm">
|
||||
{deployment.commitUserName} deployed{' '}
|
||||
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</Text>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CurrentDeployment({ deployment }: any) {
|
||||
return (
|
||||
<span className="flex flex-row">
|
||||
<Avatar
|
||||
component="span"
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
className="mr-1 h-4 w-4 self-center"
|
||||
/>
|
||||
<Text className="self-center text-sm">
|
||||
{deployment.commitUserName} updated just now
|
||||
</Text>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function checkStatusOfTheApplication(
|
||||
stateHistory: ApplicationState[] | [],
|
||||
) {
|
||||
if (stateHistory.length === 0) {
|
||||
return ApplicationStatus.Empty;
|
||||
}
|
||||
|
||||
if (stateHistory[0].stateId === undefined) {
|
||||
return ApplicationStatus.Empty;
|
||||
}
|
||||
|
||||
return stateHistory[0].stateId;
|
||||
}
|
||||
|
||||
export function RenderWorkspacesWithApps({
|
||||
userData,
|
||||
query,
|
||||
}: {
|
||||
userData: UserData | null;
|
||||
query: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
{userData?.workspaces
|
||||
.filter((workspace) =>
|
||||
workspace.applications.map((app) =>
|
||||
app.name.toLowerCase().includes(query.toLowerCase()),
|
||||
),
|
||||
)
|
||||
.sort((w1, w2) =>
|
||||
// sort alphabetical order (A-Z)
|
||||
w1.name.localeCompare(w2.name),
|
||||
)
|
||||
.map((workspace) => {
|
||||
// early exit if no applications are available
|
||||
if (workspace.applications.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceProjects = workspace.applications
|
||||
.filter((app: Application) =>
|
||||
app.name.toLowerCase().includes(query.toLowerCase()),
|
||||
)
|
||||
.sort((appA, appB) => {
|
||||
// sort apps based on either:
|
||||
// 1. When the app was recently deployed, if there is any deployments available
|
||||
// 2. When the app was created
|
||||
|
||||
const appASort =
|
||||
appA.deployments.length > 0
|
||||
? new Date(appA.deployments[0].deploymentEndedAt)
|
||||
: new Date(appA.createdAt);
|
||||
|
||||
const appBSort =
|
||||
appB.deployments.length > 0
|
||||
? new Date(appB.deployments[0].deploymentEndedAt)
|
||||
: new Date(appB.createdAt);
|
||||
|
||||
if (appASort > appBSort) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={workspace.slug} className="my-8">
|
||||
<NavLink href={`/${workspace.slug}`} passHref>
|
||||
<Link
|
||||
href={`${workspace.slug}`}
|
||||
className="mb-1.5 block font-medium"
|
||||
underline="none"
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
{workspace.name}
|
||||
</Link>
|
||||
</NavLink>
|
||||
<List className="grid grid-flow-row border-y">
|
||||
{workspaceProjects.map((app, index) => {
|
||||
const isDeployingToProduction = app.deployments[0]
|
||||
? app.deployments[0].deploymentStatus === 'DEPLOYING'
|
||||
: false;
|
||||
|
||||
return (
|
||||
<Fragment key={app.slug}>
|
||||
<ListItem.Root
|
||||
secondaryAction={
|
||||
<div className="grid grid-flow-col gap-px">
|
||||
{app.deployments[0] && (
|
||||
<div className="mr-2 flex self-center align-middle">
|
||||
<StatusCircle
|
||||
status={
|
||||
app.deployments[0]
|
||||
.deploymentStatus as DeploymentStatus
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<StateBadge
|
||||
status={checkStatusOfTheApplication(
|
||||
app.appStates,
|
||||
)}
|
||||
title={getApplicationStatusString(
|
||||
checkStatusOfTheApplication(app.appStates),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<NavLink
|
||||
href={`${workspace?.slug}/${app.slug}`}
|
||||
passHref
|
||||
>
|
||||
<ListItem.Button className="rounded-none">
|
||||
<ListItem.Avatar>
|
||||
<div className="h-10 w-10 overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
</ListItem.Avatar>
|
||||
|
||||
<ListItem.Text
|
||||
primary={app.name}
|
||||
secondary={
|
||||
<>
|
||||
{isDeployingToProduction && (
|
||||
<CurrentDeployment
|
||||
deployment={app.deployments[0]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isDeployingToProduction &&
|
||||
app.deployments[0] && (
|
||||
<LastSuccessfulDeployment
|
||||
deployment={app.deployments[0]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isDeployingToProduction &&
|
||||
!app.deployments[0] && (
|
||||
<ApplicationCreatedAt
|
||||
createdAt={app.createdAt}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ListItem.Button>
|
||||
</NavLink>
|
||||
</ListItem.Root>
|
||||
|
||||
{index < workspaceProjects.length - 1 && (
|
||||
<Divider component="li" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<FindOldApps />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Checkbox from '@/ui/v2/Checkbox';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useRestoreApplicationDatabaseMutation } from '@/utils/__generated__/graphql';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { formatISO9075 } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -28,7 +28,7 @@ export function RestoreBackupModal({
|
||||
|
||||
const [isSure, setIsSure] = useState(false);
|
||||
const [mutationIsCompleted, setMutationIsCompleted] = useState(false);
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [restoreApplicationDatabase, { loading }] =
|
||||
useRestoreApplicationDatabaseMutation();
|
||||
@@ -39,7 +39,7 @@ export function RestoreBackupModal({
|
||||
await restoreApplicationDatabase({
|
||||
variables: {
|
||||
backupId,
|
||||
appId: currentApplication.id,
|
||||
appId: currentProject.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -53,9 +53,9 @@ export function RestoreBackupModal({
|
||||
|
||||
if (mutationIsCompleted) {
|
||||
return (
|
||||
<Box className="w-modal p-6 rounded-lg">
|
||||
<Box className="w-modal rounded-lg p-6">
|
||||
<div className="flex flex-col">
|
||||
<Text className="text-center font-medium text-lg">
|
||||
<Text className="text-center text-lg font-medium">
|
||||
The backup has been restored successfully.
|
||||
</Text>
|
||||
|
||||
@@ -68,7 +68,7 @@ export function RestoreBackupModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="w-modal px-6 py-6 text-left rounded-lg">
|
||||
<Box className="w-modal rounded-lg px-6 py-6 text-left">
|
||||
<div className="flex flex-col">
|
||||
<Text className="text-center text-lg font-medium">
|
||||
Restore Database Backup
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { PropsWithChildren } from 'react';
|
||||
export function StagingMetadata({ children }: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
isDevOrStaging() && (
|
||||
<div className="mt-10">
|
||||
<div className="mx-auto mt-10 max-w-sm">
|
||||
<Box className="mx-auto flex flex-col rounded-md border p-5 text-center">
|
||||
<Status status={StatusEnum.Deploying}>Internal info</Status>
|
||||
{children}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
@@ -19,31 +20,37 @@ export function UnlockFeatureByUpgrading({
|
||||
className,
|
||||
...props
|
||||
}: UnlockFeatureByUpgradingProps) {
|
||||
const { openAlertDialog } = useDialog();
|
||||
const { openDialog } = useDialog();
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
|
||||
return (
|
||||
<div className={twMerge('flex', className)} {...props}>
|
||||
<Alert className="grid w-full grid-flow-col place-content-between items-center gap-2">
|
||||
<Text className="text-left">{message}</Text>
|
||||
<Text className="grid grid-flow-row justify-items-start gap-0.5">
|
||||
<Text component="span">{message}</Text>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() => {
|
||||
openAlertDialog({
|
||||
title: 'Upgrade your plan.',
|
||||
payload: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0' },
|
||||
hidePrimaryAction: true,
|
||||
hideSecondaryAction: true,
|
||||
hideTitle: true,
|
||||
maxWidth: 'lg',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
{!isOwner && (
|
||||
<Text component="span" color="secondary" className="text-sm">
|
||||
Ask an owner of this workspace to upgrade the project.
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{isOwner && (
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
component: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0 max-w-xl w-full' },
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
)}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ConnectGithubModalState } from '@/components/applications/ConnectGithubModal';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { EditRepositorySettingsModal } from './EditRepositorySettingsModal';
|
||||
|
||||
@@ -21,13 +21,13 @@ export function EditRepositorySettings({
|
||||
selectedRepoId,
|
||||
handleSelectAnotherRepository,
|
||||
}: EditRepositorySettingsProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const form = useForm<EditRepositorySettingsFormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
productionBranch: currentApplication.repositoryProductionBranch || 'main',
|
||||
repoBaseFolder: currentApplication.nhostBaseFolder,
|
||||
productionBranch: currentProject?.repositoryProductionBranch || 'main',
|
||||
repoBaseFolder: currentProject?.nhostBaseFolder,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@ import type { EditRepositorySettingsFormData } from '@/components/applications/g
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import ErrorBoundaryFallback from '@/components/common/ErrorBoundaryFallback';
|
||||
import GithubIcon from '@/components/icons/GithubIcon';
|
||||
import { useUpdateAppMutation } from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useUpdateApplicationMutation } from '@/generated/graphql';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { RepoAndBranch } from './RepoAndBranch';
|
||||
@@ -27,20 +25,19 @@ export function EditRepositorySettingsModal({
|
||||
const isNotCompleted = !watch('productionBranch') || !watch('repoBaseFolder');
|
||||
const { closeAlertDialog } = useDialog();
|
||||
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { currentProject, refetch: refetchProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
|
||||
const [updateApp, { loading }] = useUpdateAppMutation();
|
||||
|
||||
const client = useApolloClient();
|
||||
const [updateApp, { loading }] = useUpdateApplicationMutation();
|
||||
|
||||
const handleEditGitHubIntegration = async (
|
||||
data: EditRepositorySettingsFormData,
|
||||
) => {
|
||||
try {
|
||||
if (!currentApplication.githubRepository || selectedRepoId) {
|
||||
if (!currentProject.githubRepository || selectedRepoId) {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
appId: currentProject.id,
|
||||
app: {
|
||||
githubRepositoryId: selectedRepoId,
|
||||
repositoryProductionBranch: data.productionBranch,
|
||||
@@ -51,7 +48,7 @@ export function EditRepositorySettingsModal({
|
||||
} else {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
appId: currentProject.id,
|
||||
app: {
|
||||
repositoryProductionBranch: data.productionBranch,
|
||||
nhostBaseFolder: data.repoBaseFolder,
|
||||
@@ -60,7 +57,8 @@ export function EditRepositorySettingsModal({
|
||||
});
|
||||
}
|
||||
|
||||
await updateOwnCache(client);
|
||||
await refetchProject();
|
||||
|
||||
if (close) {
|
||||
close();
|
||||
} else {
|
||||
@@ -69,7 +67,7 @@ export function EditRepositorySettingsModal({
|
||||
triggerToast('GitHub repository settings successfully updated.');
|
||||
} catch (error) {
|
||||
await discordAnnounce(
|
||||
`Error while trying to edit repository GitHub integration: ${currentApplication.slug}.`,
|
||||
`Error while trying to edit repository GitHub integration: ${currentProject.slug}.`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Option from '@/ui/v2/Option';
|
||||
@@ -18,10 +19,12 @@ export interface UserSelectProps {
|
||||
}
|
||||
|
||||
export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const userApplicationClient = useRemoteApplicationGQLClient();
|
||||
const { data, loading, error } = useRemoteAppGetUsersCustomQuery({
|
||||
client: userApplicationClient,
|
||||
variables: { where: {}, limit: 250, offset: 0 },
|
||||
skip: !currentProject,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
@@ -36,8 +39,6 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { users } = data;
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
@@ -57,7 +58,7 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
|
||||
const user: RemoteAppGetUsersCustomQuery['users'][0] = data?.users.find(
|
||||
({ id }) => id === userId,
|
||||
);
|
||||
|
||||
@@ -68,7 +69,7 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
||||
>
|
||||
<Option value="admin">Admin</Option>
|
||||
|
||||
{users.map(({ id, displayName, email, phoneNumber }) => (
|
||||
{data?.users.map(({ id, displayName, email, phoneNumber }) => (
|
||||
<Option key={id} value={id}>
|
||||
{displayName || email || phoneNumber || id}
|
||||
</Option>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { BoxProps } from '@/ui/v2/Box';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Text from '@/ui/v2/Text';
|
||||
@@ -10,8 +10,7 @@ export interface BreadcrumbsProps extends BoxProps {}
|
||||
|
||||
export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
if (!isPlatform) {
|
||||
return (
|
||||
@@ -31,6 +30,7 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
|
||||
<NavLink
|
||||
href="/local/local"
|
||||
className="truncate text-[13px] hover:underline sm:text-sm"
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
local
|
||||
</NavLink>
|
||||
@@ -53,21 +53,23 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
|
||||
<NavLink
|
||||
href={`/${currentWorkspace.slug}`}
|
||||
className="truncate text-[13px] hover:underline sm:text-sm"
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
{currentWorkspace.name}
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentApplication && (
|
||||
{currentProject && (
|
||||
<>
|
||||
<Text color="disabled">/</Text>
|
||||
|
||||
<NavLink
|
||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}`}
|
||||
href={`/${currentWorkspace.slug}/${currentProject.slug}`}
|
||||
className="truncate text-[13px] hover:underline sm:text-sm"
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
{currentApplication.name}
|
||||
{currentProject.name}
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -52,7 +52,9 @@ function ControlledAutocomplete(
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
inputValue={typeof field.value === 'string' ? field.value : undefined}
|
||||
inputValue={
|
||||
typeof field.value !== 'object' ? field.value.toString() : undefined
|
||||
}
|
||||
{...props}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@/utils/testUtils';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import type { Column } from 'react-table';
|
||||
import { expect, test } from 'vitest';
|
||||
import DataGrid from './DataGrid';
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function DataGridDateCell<TData extends object>({
|
||||
: undefined;
|
||||
|
||||
const { year, month, day, hour, minute, second } = getDateComponents(date, {
|
||||
adjustTimezone: specificType === 'timetz' || specificType === 'timestamptz',
|
||||
adjustTimezone: ['date', 'timetz', 'timestamptz'].includes(specificType),
|
||||
});
|
||||
|
||||
const { inputRef, focusCell, isEditing, cancelEditCell } =
|
||||
|
||||
@@ -2,8 +2,8 @@ import AudioPreview from '@/components/icons/AudioPreview';
|
||||
import { FileIcon } from '@/components/icons/FileIcon';
|
||||
import PDFPreview from '@/components/icons/PDFPreview';
|
||||
import VideoPreview from '@/components/icons/VideoPreview';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useAppClient } from '@/hooks/useAppClient';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Box from '@/ui/v2/Box';
|
||||
@@ -166,7 +166,7 @@ export default function DataGridPreviewCell<TData extends object>({
|
||||
value: { fetchBlob, id, mimeType, alt, blob },
|
||||
fallbackPreview = null,
|
||||
}: DataGridPreviewCellProps<TData>) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const appClient = useAppClient();
|
||||
const { objectUrl, loading, error } = useBlob({ fetchBlob, blob, mimeType });
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@@ -205,7 +205,7 @@ export default function DataGridPreviewCell<TData extends object>({
|
||||
}
|
||||
|
||||
const { presignedUrl } = await appClient.storage
|
||||
.setAdminSecret(currentApplication.hasuraGraphqlAdminSecret)
|
||||
.setAdminSecret(currentProject?.config?.hasura.adminSecret)
|
||||
.getPresignedUrl({ fileId: id });
|
||||
|
||||
if (!presignedUrl) {
|
||||
|
||||
@@ -1,31 +1,8 @@
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import type { CommonDialogProps } from '@/ui/v2/Dialog';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { createContext } from 'react';
|
||||
|
||||
/**
|
||||
* Available dialog types.
|
||||
*/
|
||||
export type DialogType =
|
||||
| 'EDIT_WORKSPACE_NAME'
|
||||
| 'CREATE_RECORD'
|
||||
| 'CREATE_COLUMN'
|
||||
| 'EDIT_COLUMN'
|
||||
| 'CREATE_TABLE'
|
||||
| 'EDIT_TABLE'
|
||||
| 'EDIT_PERMISSIONS'
|
||||
| 'CREATE_FOREIGN_KEY'
|
||||
| 'EDIT_FOREIGN_KEY'
|
||||
| 'CREATE_ROLE'
|
||||
| 'EDIT_ROLE'
|
||||
| 'CREATE_USER'
|
||||
| 'CREATE_PERMISSION_VARIABLE'
|
||||
| 'EDIT_PERMISSION_VARIABLE'
|
||||
| 'CREATE_ENVIRONMENT_VARIABLE'
|
||||
| 'EDIT_ENVIRONMENT_VARIABLE'
|
||||
| 'EDIT_USER'
|
||||
| 'EDIT_USER_PASSWORD'
|
||||
| 'EDIT_JWT_SECRET';
|
||||
|
||||
export interface DialogConfig<TPayload = unknown> {
|
||||
/**
|
||||
* Title of the dialog.
|
||||
@@ -41,21 +18,36 @@ export interface DialogConfig<TPayload = unknown> {
|
||||
payload?: TPayload;
|
||||
}
|
||||
|
||||
export interface OpenDialogOptions {
|
||||
/**
|
||||
* Title of the dialog.
|
||||
*/
|
||||
title?: ReactNode;
|
||||
/**
|
||||
* Component to render inside the dialog skeleton.
|
||||
*/
|
||||
component: ReactElement<{
|
||||
location?: 'drawer' | 'dialog';
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (args?: any) => Promise<any> | void;
|
||||
}>;
|
||||
/**
|
||||
* Props to pass to the root dialog component.
|
||||
*/
|
||||
props?: Partial<CommonDialogProps>;
|
||||
}
|
||||
|
||||
export interface DialogContextProps {
|
||||
/**
|
||||
* Call this function to open a dialog.
|
||||
* Call this function to open a dialog. It will automatically apply the
|
||||
* necessary functionality to the dialog.
|
||||
*/
|
||||
openDialog: <TPayload = unknown>(
|
||||
type: DialogType,
|
||||
config?: DialogConfig<TPayload>,
|
||||
) => void;
|
||||
openDialog: (options: OpenDialogOptions) => void;
|
||||
/**
|
||||
* Call this function to open a drawer.
|
||||
* Call this function to open a drawer. It will automatically apply the
|
||||
* necessary functionality to the drawer.
|
||||
*/
|
||||
openDrawer: <TPayload = unknown>(
|
||||
type: DialogType,
|
||||
config?: DialogConfig<TPayload>,
|
||||
) => void;
|
||||
openDrawer: (options: OpenDialogOptions) => void;
|
||||
/**
|
||||
* Call this function to open an alert dialog.
|
||||
*/
|
||||
@@ -87,7 +79,7 @@ export interface DialogContextProps {
|
||||
*/
|
||||
onDirtyStateChange: (
|
||||
isDirty: boolean,
|
||||
location?: 'drawer' | 'dialog',
|
||||
location?: DialogFormProps['location'],
|
||||
) => void;
|
||||
/**
|
||||
* Call this function to open a dirty confirmation dialog.
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
|
||||
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
|
||||
import EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
|
||||
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
||||
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
||||
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
|
||||
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
|
||||
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
|
||||
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
|
||||
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
|
||||
import CreateUserForm from '@/components/users/CreateUserForm';
|
||||
import EditUserForm from '@/components/users/EditUserForm';
|
||||
import EditUserPasswordForm from '@/components/users/EditUserPasswordForm';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import AlertDialog from '@/ui/v2/AlertDialog';
|
||||
import { BaseDialog } from '@/ui/v2/Dialog';
|
||||
import Drawer from '@/ui/v2/Drawer';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
import type {
|
||||
BaseSyntheticEvent,
|
||||
DetailedHTMLProps,
|
||||
HTMLProps,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
import type { BaseSyntheticEvent, PropsWithChildren } from 'react';
|
||||
import {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -33,7 +15,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DialogConfig, DialogType } from './DialogContext';
|
||||
import type { DialogConfig, OpenDialogOptions } from './DialogContext';
|
||||
import DialogContext from './DialogContext';
|
||||
import {
|
||||
alertDialogReducer,
|
||||
@@ -41,67 +23,11 @@ import {
|
||||
drawerReducer,
|
||||
} from './dialogReducers';
|
||||
|
||||
function LoadingComponent({
|
||||
className,
|
||||
...props
|
||||
}: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> = {}) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
'grid items-center justify-center px-6 py-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ActivityIndicator
|
||||
circularProgressProps={{ className: 'w-5 h-5' }}
|
||||
label="Loading form..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CreateRecordForm = dynamic(
|
||||
() => import('@/components/dataBrowser/CreateRecordForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateColumnForm = dynamic(
|
||||
() => import('@/components/dataBrowser/CreateColumnForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditColumnForm = dynamic(
|
||||
() => import('@/components/dataBrowser/EditColumnForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateTableForm = dynamic(
|
||||
() => import('@/components/dataBrowser/CreateTableForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditTableForm = dynamic(
|
||||
() => import('@/components/dataBrowser/EditTableForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditPermissionsForm = dynamic(
|
||||
() => import('@/components/dataBrowser/EditPermissionsForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const router = useRouter();
|
||||
|
||||
const [
|
||||
{
|
||||
open: dialogOpen,
|
||||
activeDialogType,
|
||||
dialogProps,
|
||||
title: dialogTitle,
|
||||
payload: dialogPayload,
|
||||
},
|
||||
{ open: dialogOpen, title: dialogTitle, activeDialog, dialogProps },
|
||||
dialogDispatch,
|
||||
] = useReducer(dialogReducer, {
|
||||
open: false,
|
||||
@@ -110,10 +36,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const [
|
||||
{
|
||||
open: drawerOpen,
|
||||
activeDialogType: activeDrawerType,
|
||||
dialogProps: drawerProps,
|
||||
title: drawerTitle,
|
||||
payload: drawerPayload,
|
||||
activeDialog: activeDrawer,
|
||||
dialogProps: drawerProps,
|
||||
},
|
||||
drawerDispatch,
|
||||
] = useReducer(drawerReducer, {
|
||||
@@ -136,12 +61,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const isDialogDirty = useRef(false);
|
||||
const [showDirtyConfirmation, setShowDirtyConfirmation] = useState(false);
|
||||
|
||||
const openDialog = useCallback(
|
||||
<TConfig,>(type: DialogType, config?: DialogConfig<TConfig>) => {
|
||||
dialogDispatch({ type: 'OPEN_DIALOG', payload: { type, config } });
|
||||
},
|
||||
[],
|
||||
);
|
||||
const openDialog = useCallback((options: OpenDialogOptions) => {
|
||||
dialogDispatch({ type: 'OPEN_DIALOG', payload: options });
|
||||
}, []);
|
||||
|
||||
const closeDialog = useCallback(() => {
|
||||
dialogDispatch({ type: 'HIDE_DIALOG' });
|
||||
@@ -152,12 +74,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
dialogDispatch({ type: 'CLEAR_DIALOG_CONTENT' });
|
||||
}, []);
|
||||
|
||||
const openDrawer = useCallback(
|
||||
<TConfig,>(type: DialogType, config?: DialogConfig<TConfig>) => {
|
||||
drawerDispatch({ type: 'OPEN_DRAWER', payload: { type, config } });
|
||||
},
|
||||
[],
|
||||
);
|
||||
const openDrawer = useCallback((options: OpenDialogOptions) => {
|
||||
drawerDispatch({ type: 'OPEN_DRAWER', payload: options });
|
||||
}, []);
|
||||
|
||||
const closeDrawer = useCallback(() => {
|
||||
drawerDispatch({ type: 'HIDE_DRAWER' });
|
||||
@@ -228,9 +147,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
[closeDialog, openDirtyConfirmation],
|
||||
);
|
||||
|
||||
// We are coupling this logic with the location of the dialog content which is
|
||||
// not ideal. We shoule figure out a better logic for tracking the dirty
|
||||
// state in the future.
|
||||
const onDirtyStateChange = useCallback(
|
||||
(dirty: boolean, location: 'drawer' | 'dialog' = 'drawer') => {
|
||||
if (location === 'dialog') {
|
||||
@@ -271,25 +187,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
],
|
||||
);
|
||||
|
||||
const sharedDialogProps = {
|
||||
...dialogPayload,
|
||||
onSubmit: async (values: any) => {
|
||||
await dialogPayload?.onSubmit?.(values);
|
||||
|
||||
closeDialog();
|
||||
},
|
||||
onCancel: closeDialogWithDirtyGuard,
|
||||
};
|
||||
|
||||
const sharedDrawerProps = {
|
||||
onSubmit: async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
},
|
||||
onCancel: closeDrawerWithDirtyGuard,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function handleCloseDrawerAndDialog() {
|
||||
if (isDrawerDirty.current || isDialogDirty.current) {
|
||||
@@ -367,56 +264,20 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<RetryableErrorBoundary
|
||||
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
|
||||
>
|
||||
{activeDialogType === 'EDIT_WORKSPACE_NAME' && (
|
||||
<EditWorkspaceNameForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_FOREIGN_KEY' && (
|
||||
<CreateForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_FOREIGN_KEY' && (
|
||||
<EditForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_ROLE' && (
|
||||
<CreateRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_ROLE' && (
|
||||
<EditRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_USER' && (
|
||||
<CreateUserForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_PERMISSION_VARIABLE' && (
|
||||
<CreatePermissionVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_PERMISSION_VARIABLE' && (
|
||||
<EditPermissionVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_ENVIRONMENT_VARIABLE' && (
|
||||
<CreateEnvironmentVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && (
|
||||
<EditEnvironmentVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_USER_PASSWORD' && (
|
||||
<EditUserPasswordForm
|
||||
{...sharedDialogProps}
|
||||
user={sharedDialogProps?.user}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_JWT_SECRET' && (
|
||||
<EditJwtSecretForm {...sharedDialogProps} />
|
||||
)}
|
||||
{isValidElement(activeDialog)
|
||||
? cloneElement(activeDialog, {
|
||||
...activeDialog.props,
|
||||
location: 'dialog',
|
||||
onSubmit: async (values?: any) => {
|
||||
await activeDialog?.props?.onSubmit?.(values);
|
||||
closeDialog();
|
||||
},
|
||||
onCancel: () => {
|
||||
activeDialog?.props?.onCancel?.();
|
||||
closeDialogWithDirtyGuard();
|
||||
},
|
||||
})
|
||||
: null}
|
||||
</RetryableErrorBoundary>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -436,51 +297,20 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
}}
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
{activeDrawerType === 'CREATE_RECORD' && (
|
||||
<CreateRecordForm
|
||||
{...sharedDrawerProps}
|
||||
columns={drawerPayload?.columns}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'CREATE_COLUMN' && (
|
||||
<CreateColumnForm {...sharedDrawerProps} />
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_COLUMN' && (
|
||||
<EditColumnForm
|
||||
{...sharedDrawerProps}
|
||||
column={drawerPayload?.column}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'CREATE_TABLE' && (
|
||||
<CreateTableForm
|
||||
{...sharedDrawerProps}
|
||||
schema={drawerPayload?.schema}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_TABLE' && (
|
||||
<EditTableForm
|
||||
{...sharedDrawerProps}
|
||||
table={drawerPayload?.table}
|
||||
schema={drawerPayload?.schema}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_PERMISSIONS' && (
|
||||
<EditPermissionsForm
|
||||
{...sharedDrawerProps}
|
||||
disabled={drawerPayload?.disabled}
|
||||
schema={drawerPayload?.schema}
|
||||
table={drawerPayload?.table}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_USER' && (
|
||||
<EditUserForm {...sharedDrawerProps} {...drawerPayload} />
|
||||
)}
|
||||
{isValidElement(activeDrawer)
|
||||
? cloneElement(activeDrawer, {
|
||||
...activeDrawer.props,
|
||||
location: 'drawer',
|
||||
onSubmit: async (values?: any) => {
|
||||
await activeDrawer?.props?.onSubmit?.(values);
|
||||
closeDrawer();
|
||||
},
|
||||
onCancel: () => {
|
||||
activeDrawer?.props?.onCancel?.();
|
||||
closeDrawerWithDirtyGuard();
|
||||
},
|
||||
})
|
||||
: null}
|
||||
</RetryableErrorBoundary>
|
||||
</Drawer>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CommonDialogProps } from '@/ui/v2/Dialog';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { DialogConfig, DialogType } from './DialogContext';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import type { DialogConfig, OpenDialogOptions } from './DialogContext';
|
||||
|
||||
export interface DialogState {
|
||||
/**
|
||||
@@ -12,9 +12,13 @@ export interface DialogState {
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* Type of the currently active dialog.
|
||||
* Component to render inside the dialog skeleton.
|
||||
*/
|
||||
activeDialogType?: DialogType;
|
||||
activeDialog?: ReactElement<{
|
||||
location?: 'drawer' | 'dialog';
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (args?: any) => Promise<any> | void;
|
||||
}>;
|
||||
/**
|
||||
* Props passed to the currently active dialog.
|
||||
*/
|
||||
@@ -27,10 +31,7 @@ export interface DialogState {
|
||||
}
|
||||
|
||||
export type DialogAction =
|
||||
| {
|
||||
type: 'OPEN_DIALOG';
|
||||
payload: { type: DialogType; config?: DialogConfig };
|
||||
}
|
||||
| { type: 'OPEN_DIALOG'; payload: OpenDialogOptions }
|
||||
| { type: 'HIDE_DIALOG' }
|
||||
| { type: 'CLEAR_DIALOG_CONTENT' };
|
||||
|
||||
@@ -50,10 +51,9 @@ export function dialogReducer(
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
activeDialogType: action.payload?.type,
|
||||
dialogProps: action.payload.config?.props,
|
||||
title: action.payload.config?.title,
|
||||
payload: action.payload.config?.payload,
|
||||
title: action.payload.title,
|
||||
activeDialog: action.payload.component,
|
||||
dialogProps: action.payload.props,
|
||||
};
|
||||
case 'HIDE_DIALOG':
|
||||
return {
|
||||
@@ -64,8 +64,7 @@ export function dialogReducer(
|
||||
return {
|
||||
...state,
|
||||
title: undefined,
|
||||
payload: undefined,
|
||||
activeDialogType: undefined,
|
||||
activeDialog: undefined,
|
||||
dialogProps: undefined,
|
||||
};
|
||||
default:
|
||||
@@ -74,10 +73,7 @@ export function dialogReducer(
|
||||
}
|
||||
|
||||
export type DrawerAction =
|
||||
| {
|
||||
type: 'OPEN_DRAWER';
|
||||
payload: { type: DialogType; config?: DialogConfig };
|
||||
}
|
||||
| { type: 'OPEN_DRAWER'; payload: OpenDialogOptions }
|
||||
| { type: 'HIDE_DRAWER' }
|
||||
| { type: 'CLEAR_DRAWER_CONTENT' };
|
||||
|
||||
@@ -97,10 +93,9 @@ export function drawerReducer(
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
activeDialogType: action.payload?.type,
|
||||
dialogProps: action.payload.config?.props,
|
||||
title: action.payload.config?.title,
|
||||
payload: action.payload.config?.payload,
|
||||
title: action.payload.title,
|
||||
activeDialog: action.payload.component,
|
||||
dialogProps: action.payload.props,
|
||||
};
|
||||
case 'HIDE_DRAWER':
|
||||
return {
|
||||
@@ -111,8 +106,7 @@ export function drawerReducer(
|
||||
return {
|
||||
...state,
|
||||
title: undefined,
|
||||
payload: undefined,
|
||||
activeDialogType: undefined,
|
||||
activeDialog: undefined,
|
||||
dialogProps: undefined,
|
||||
};
|
||||
default:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './DialogContext';
|
||||
export { default as DialogContext } from './DialogContext';
|
||||
export { default as DialogProvider } from './DialogProvider';
|
||||
export { default as useDialog } from './useDialog';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BoxProps } from '@/ui/v2/Box';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface FormProps extends BoxProps {
|
||||
@@ -11,6 +12,7 @@ export interface FormProps extends BoxProps {
|
||||
}
|
||||
|
||||
export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
||||
const formRef = useRef<HTMLDivElement>();
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
@@ -25,6 +27,15 @@ export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const submitButton = Array.from(
|
||||
formRef.current.getElementsByTagName('button'),
|
||||
).find((item) => item.type === 'submit');
|
||||
|
||||
// Disabling submit if the submit button is disabled
|
||||
if (submitButton?.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
handleSubmit(onSubmit)(event);
|
||||
@@ -35,6 +46,7 @@ export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
||||
// so keyboard events must be handled on the form element itself.
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<Box
|
||||
ref={formRef}
|
||||
component="form"
|
||||
{...props}
|
||||
onKeyDown={(event) => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { BoxProps } from '@/ui/v2/Box';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface FormActivityIndicatorProps extends BoxProps {}
|
||||
|
||||
export default function FormActivityIndicator({
|
||||
className,
|
||||
...props
|
||||
}: FormActivityIndicatorProps) {
|
||||
return (
|
||||
<Box
|
||||
{...props}
|
||||
className={twMerge(
|
||||
'grid items-center justify-center px-6 py-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ActivityIndicator
|
||||
circularProgressProps={{ className: 'w-5 h-5' }}
|
||||
label="Loading form..."
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './FormActivityIndicator';
|
||||
export { default } from './FormActivityIndicator';
|
||||
@@ -1,14 +1,13 @@
|
||||
import Breadcrumbs from '@/components/common/Breadcrumbs';
|
||||
import FeedbackForm from '@/components/common/FeedbackForm';
|
||||
import LocalAccountMenu from '@/components/common/LocalAccountMenu';
|
||||
import Logo from '@/components/common/Logo';
|
||||
import MobileNav from '@/components/common/MobileNav';
|
||||
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import { AccountMenu } from '@/components/dashboard/AccountMenu';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import NavLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { DetailedHTMLProps, HTMLProps, PropsWithoutRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -24,6 +23,7 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="header"
|
||||
className={twMerge(
|
||||
'z-40 grid w-full transform-gpu grid-flow-col items-center justify-between gap-2 border-b-1 px-4 py-3',
|
||||
className,
|
||||
@@ -32,10 +32,8 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
{...props}
|
||||
>
|
||||
<div className="grid grid-flow-col items-center gap-3">
|
||||
<NavLink href="/" passHref>
|
||||
<Link href="/" className="w-12">
|
||||
<Logo className="mx-auto cursor-pointer" />
|
||||
</Link>
|
||||
<NavLink href="/" className="w-12">
|
||||
<Logo className="mx-auto cursor-pointer" />
|
||||
</NavLink>
|
||||
|
||||
{(router.query.workspaceSlug || router.query.appSlug) && (
|
||||
@@ -63,25 +61,20 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
)}
|
||||
|
||||
<NavLink
|
||||
underline="none"
|
||||
href="https://docs.nhost.io"
|
||||
passHref
|
||||
className="mr-2 rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
'&:hover': { backgroundColor: 'grey.200' },
|
||||
}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Link
|
||||
underline="none"
|
||||
href="https://docs.nhost.io"
|
||||
className="mr-2 rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
'&:hover': { backgroundColor: 'grey.200' },
|
||||
}}
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
Docs
|
||||
</NavLink>
|
||||
|
||||
{isPlatform ? <AccountMenu /> : <ThemeSwitcher className="w-52" />}
|
||||
{isPlatform ? <AccountMenu /> : <LocalAccountMenu />}
|
||||
</div>
|
||||
|
||||
<MobileNav className="sm:hidden" />
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { NavLinkProps } from '@/components/common/NavLink';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import type { SvgIconProps } from '@/ui/v2/icons/SvgIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import NavLink from 'next/link';
|
||||
import type { ForwardedRef, ReactElement } from 'react';
|
||||
import type { ForwardedRef, PropsWithoutRef, ReactElement } from 'react';
|
||||
import { cloneElement, forwardRef, isValidElement } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface IconLinkProps extends Omit<NavLinkProps, 'ref'> {
|
||||
export interface IconLinkProps extends PropsWithoutRef<NavLinkProps> {
|
||||
/**
|
||||
* The icon to display.
|
||||
*/
|
||||
@@ -39,6 +38,12 @@ function IconLink(
|
||||
: [icon.props?.sx]),
|
||||
{
|
||||
color: (theme) => {
|
||||
if (props.disabled) {
|
||||
return theme.palette.mode === 'dark'
|
||||
? 'text.secondary'
|
||||
: 'text.primary';
|
||||
}
|
||||
|
||||
if (active) {
|
||||
return 'primary.main';
|
||||
}
|
||||
@@ -58,49 +63,49 @@ function IconLink(
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink ref={ref} passHref href={href} {...props}>
|
||||
<Link
|
||||
href={href}
|
||||
underline="none"
|
||||
className={twMerge(
|
||||
'grid grid-flow-row justify-items-center gap-1 rounded-md py-2.5 px-0.5 text-center font-medium motion-safe:transition-colors',
|
||||
className,
|
||||
)}
|
||||
sx={{
|
||||
fontSize: (theme) => theme.typography.pxToRem(10),
|
||||
lineHeight: (theme) => theme.typography.pxToRem(15),
|
||||
backgroundColor: active ? 'primary.light' : 'transparent',
|
||||
color: active ? 'primary.main' : 'text.primary',
|
||||
[`&:hover`]: {
|
||||
backgroundColor: active ? 'primary.light' : 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isValidElement(icon)
|
||||
? cloneElement(icon, {
|
||||
...icon.props,
|
||||
className: twMerge('w-4 h-4', icon.props.className),
|
||||
sx: [
|
||||
...(Array.isArray(icon.props?.sx)
|
||||
? icon.props.sx
|
||||
: [icon.props?.sx]),
|
||||
{
|
||||
color: (theme) => {
|
||||
if (active) {
|
||||
return 'primary.main';
|
||||
}
|
||||
<NavLink
|
||||
ref={ref}
|
||||
href={href}
|
||||
underline="none"
|
||||
className={twMerge(
|
||||
'grid grid-flow-row justify-items-center gap-1 rounded-md py-2.5 px-0.5 text-center font-medium motion-safe:transition-colors',
|
||||
className,
|
||||
)}
|
||||
sx={{
|
||||
fontSize: (theme) => theme.typography.pxToRem(10),
|
||||
lineHeight: (theme) => theme.typography.pxToRem(15),
|
||||
backgroundColor: active ? 'primary.light' : 'transparent',
|
||||
color: active ? 'primary.main' : 'text.primary',
|
||||
[`&:hover`]: {
|
||||
backgroundColor: active ? 'primary.light' : 'action.hover',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{isValidElement(icon)
|
||||
? cloneElement(icon, {
|
||||
...icon.props,
|
||||
className: twMerge('w-4 h-4', icon.props.className),
|
||||
sx: [
|
||||
...(Array.isArray(icon.props?.sx)
|
||||
? icon.props.sx
|
||||
: [icon.props?.sx]),
|
||||
{
|
||||
color: (theme) => {
|
||||
if (active) {
|
||||
return 'primary.main';
|
||||
}
|
||||
|
||||
return theme.palette.mode === 'dark'
|
||||
? 'text.secondary'
|
||||
: 'text.primary';
|
||||
},
|
||||
return theme.palette.mode === 'dark'
|
||||
? 'text.secondary'
|
||||
: 'text.primary';
|
||||
},
|
||||
],
|
||||
})
|
||||
: null}
|
||||
},
|
||||
],
|
||||
})
|
||||
: null}
|
||||
|
||||
{children}
|
||||
</Link>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,50 @@
|
||||
import type { ActivityIndicatorProps } from '@/ui/v2/ActivityIndicator';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { BoxProps } from '@/ui/v2/Box';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function LoadingScreen() {
|
||||
export interface LoadingScreenProps extends BoxProps {
|
||||
/**
|
||||
* Props passed to individual component slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props passed to the `<Box />` component.
|
||||
*/
|
||||
root?: BoxProps;
|
||||
/**
|
||||
* Props passed to the `<ActivityIndicator />` component.
|
||||
*/
|
||||
activityIndicator?: ActivityIndicatorProps;
|
||||
};
|
||||
}
|
||||
|
||||
export function LoadingScreen({
|
||||
className,
|
||||
slotProps = { root: {}, activityIndicator: {} },
|
||||
...props
|
||||
}: LoadingScreenProps) {
|
||||
return (
|
||||
<Box className="absolute top-0 left-0 bottom-0 right-0 flex items-center justify-center z-50 h-full w-full">
|
||||
<ActivityIndicator circularProgressProps={{ className: 'w-5 h-5' }} />
|
||||
<Box
|
||||
className={twMerge(
|
||||
'absolute top-0 left-0 bottom-0 right-0 z-50 flex h-full w-full items-center justify-center',
|
||||
className,
|
||||
slotProps?.root?.className,
|
||||
)}
|
||||
{...slotProps?.root}
|
||||
{...props}
|
||||
>
|
||||
<ActivityIndicator
|
||||
{...slotProps?.activityIndicator}
|
||||
circularProgressProps={{
|
||||
...slotProps?.activityIndicator?.circularProgressProps,
|
||||
className: twMerge(
|
||||
'w-5 h-5',
|
||||
slotProps?.activityIndicator?.circularProgressProps?.className,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import UserIcon from '@/ui/v2/icons/UserIcon';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import getConfig from 'next/config';
|
||||
|
||||
export default function LocalAccountMenu() {
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
return (
|
||||
<Dropdown.Root className="justify-self-center">
|
||||
<Dropdown.Trigger hideChevron asChild>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="h-7 w-7 rounded-full"
|
||||
sx={{
|
||||
backgroundColor: (theme) => `${theme.palette.grey[300]} !important`,
|
||||
}}
|
||||
>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
PaperProps={{
|
||||
className: 'mt-1 p-6 grid grid-flow-row gap-4 w-full max-w-xs',
|
||||
}}
|
||||
>
|
||||
<ThemeSwitcher label="Theme" />
|
||||
|
||||
<Text className="text-center text-xs" color="disabled">
|
||||
Dashboard Version: {publicRuntimeConfig?.version || 'n/a'}
|
||||
</Text>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LocalAccountMenu';
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useUI } from '@/context/UIContext';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
|
||||
export default function MaintenanceAlert() {
|
||||
const { maintenanceActive, maintenanceEndDate } = useUI();
|
||||
|
||||
if (!maintenanceActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateTimeFormat = Intl.DateTimeFormat(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
|
||||
const parts = dateTimeFormat.formatToParts(maintenanceEndDate);
|
||||
|
||||
const year = parts.find((part) => part.type === 'year')?.value;
|
||||
const month = parts.find((part) => part.type === 'month')?.value;
|
||||
const day = parts.find((part) => part.type === 'day')?.value;
|
||||
const hour = parts.find((part) => part.type === 'hour')?.value;
|
||||
const minute = parts.find((part) => part.type === 'minute')?.value;
|
||||
const timeZone = parts.find((part) => part.type === 'timeZoneName')?.value;
|
||||
|
||||
return (
|
||||
<Alert severity="warning" className="mt-4">
|
||||
<p>
|
||||
We're currently doing maintenance on our infrastructure. Project
|
||||
creation and project settings are temporarily disabled during the
|
||||
maintenance period.
|
||||
</p>
|
||||
|
||||
{maintenanceEndDate && (
|
||||
<p>
|
||||
Maintenance is expected to be completed at {year}-{month}-{day} {hour}
|
||||
:{minute} {timeZone}.
|
||||
</p>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './MaintenanceAlert';
|
||||
@@ -3,7 +3,6 @@ import FeedbackForm from '@/components/common/FeedbackForm';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
|
||||
import { Nav } from '@/components/dashboard/Nav';
|
||||
import { useUserDataContext } from '@/context/workspace1-context';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import useProjectRoutes from '@/hooks/common/useProjectRoutes';
|
||||
import { useNavigationVisible } from '@/hooks/useNavigationVisible';
|
||||
@@ -19,7 +18,9 @@ import List from '@/ui/v2/List';
|
||||
import type { ListItemButtonProps } from '@/ui/v2/ListItem';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useSignOut } from '@nhost/nextjs';
|
||||
import getConfig from 'next/config';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactNode } from 'react';
|
||||
import { cloneElement, Fragment, isValidElement, useState } from 'react';
|
||||
@@ -87,8 +88,9 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
||||
const { signOut } = useSignOut();
|
||||
const { setUserContext } = useUserDataContext();
|
||||
const apolloClient = useApolloClient();
|
||||
const router = useRouter();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -234,7 +236,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
setShowChangePasswordModal(true);
|
||||
}}
|
||||
>
|
||||
Change password
|
||||
Change Password
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
|
||||
@@ -246,16 +248,20 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
color="secondary"
|
||||
className="justify-start border-none px-2 py-2.5 text-[16px]"
|
||||
onClick={async () => {
|
||||
setUserContext({ workspaces: [] });
|
||||
setMenuOpen(false);
|
||||
await apolloClient.clearStore();
|
||||
await signOut();
|
||||
await router.push('/signin');
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
Sign Out
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
</List>
|
||||
|
||||
<Text className="text-center text-xs" color="secondary">
|
||||
Dashboard Version: {publicRuntimeConfig?.version || 'n/a'}
|
||||
</Text>
|
||||
</section>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import Link from 'next/link';
|
||||
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
|
||||
import type { LinkProps } from '@/ui/v2/Link';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import NextLink from 'next/link';
|
||||
import type { ForwardedRef, PropsWithoutRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface NavLinkProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLAnchorElement>, HTMLAnchorElement> {}
|
||||
export interface NavLinkProps extends PropsWithoutRef<LinkProps> {
|
||||
/**
|
||||
* Determines whether or not the link should be disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function NavLink(
|
||||
{ className, children, href, ...props }: NavLinkProps,
|
||||
ref: ForwardedRef<HTMLAnchorElement>,
|
||||
) {
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<a className={twMerge('font-display', className)} ref={ref} {...props}>
|
||||
<NextLink href={href} passHref>
|
||||
<Link className={twMerge('font-display', className)} ref={ref} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
</Link>
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||