Compare commits
612 Commits
@nhost/nex
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21fb316655 | ||
|
|
9c4f350508 | ||
|
|
eb3ba21afc | ||
|
|
a84ac62999 | ||
|
|
f32bfed9a9 | ||
|
|
55632506e4 | ||
|
|
e146d32e69 | ||
|
|
df8a13997c | ||
|
|
c488fd4c0c | ||
|
|
a6e8569822 | ||
|
|
a8023c9a3f | ||
|
|
59347fcd4b | ||
|
|
5b65cac91e | ||
|
|
331ae02e2d | ||
|
|
3b8b3be393 | ||
|
|
9fb4d82d86 | ||
|
|
37d836b9b6 | ||
|
|
3c1996b13b | ||
|
|
e3d90fd5d2 | ||
|
|
af016e1caa | ||
|
|
963f9b5e85 | ||
|
|
ed4c780115 | ||
|
|
260997c6fe | ||
|
|
941f0f5755 | ||
|
|
75344b2bc0 | ||
|
|
65da426e8b | ||
|
|
a711727e94 | ||
|
|
8e8f6fd9c9 | ||
|
|
a1c2e5d8ee | ||
|
|
5c276ae844 | ||
|
|
f34702f3c5 | ||
|
|
6cb70eee01 | ||
|
|
9395c9687f | ||
|
|
eb1eb934a4 | ||
|
|
c62fed2c9a | ||
|
|
16fe1a47da | ||
|
|
0f04e8b8b8 | ||
|
|
e6dad4d696 | ||
|
|
bcb3b79add | ||
|
|
fe658231b4 | ||
|
|
a1188b7d98 | ||
|
|
cd4bdc581d | ||
|
|
4e2f8ccd52 | ||
|
|
8a6d8c7534 | ||
|
|
fa75409f09 | ||
|
|
74662052ae | ||
|
|
2de904c865 | ||
|
|
37ab5fe878 | ||
|
|
be9af96fa7 | ||
|
|
31abbe5f30 | ||
|
|
268b461d5b | ||
|
|
58af592cfa | ||
|
|
0e9d623c69 | ||
|
|
412a290646 | ||
|
|
123add38a4 | ||
|
|
5bdd31ad36 | ||
|
|
5121851c8b | ||
|
|
8ca1f92491 | ||
|
|
5535b9085b | ||
|
|
bc51122b25 | ||
|
|
b060e5e550 | ||
|
|
6a906b22e2 | ||
|
|
860c9d1be4 | ||
|
|
9eec3e58f5 | ||
|
|
4e01a43e94 | ||
|
|
c126b20dcf | ||
|
|
b727a24a5f | ||
|
|
ecadd7e1b9 | ||
|
|
2d661174a8 | ||
|
|
fcb3e5192f | ||
|
|
66fdc63f38 | ||
|
|
fa37cb6171 | ||
|
|
c1bea1294d | ||
|
|
8af2f6e9dd | ||
|
|
e3d0b96917 | ||
|
|
43705b992d | ||
|
|
2e999e8715 | ||
|
|
0370696d5c | ||
|
|
f62131d55a | ||
|
|
36c3519cf8 | ||
|
|
86d077ac00 | ||
|
|
a21aa05b5a | ||
|
|
200e9f774c | ||
|
|
9b52e9bf13 | ||
|
|
bc1235de3b | ||
|
|
fce58ebaea | ||
|
|
452e281120 | ||
|
|
9a338e54c9 | ||
|
|
baeebf980d | ||
|
|
ac92c6ee61 | ||
|
|
1ddaf680c0 | ||
|
|
c6e6194d8e | ||
|
|
83deea8b45 | ||
|
|
07c8d90053 | ||
|
|
acbaabcf85 | ||
|
|
a2621e40a4 | ||
|
|
3534501f37 | ||
|
|
27bc23cbbc | ||
|
|
61120a137a | ||
|
|
faea8feb2e | ||
|
|
6450223558 | ||
|
|
a62a85a777 | ||
|
|
ae24f83953 | ||
|
|
fc60d7a782 | ||
|
|
6be8a998df | ||
|
|
ea091f6251 | ||
|
|
8175c052f7 | ||
|
|
e6605a6ed0 | ||
|
|
1cba0e6492 | ||
|
|
179c90fcdb | ||
|
|
552e31a4f0 | ||
|
|
85f0f943a1 | ||
|
|
c4c23fde31 | ||
|
|
e0b94c3e90 | ||
|
|
113d638532 | ||
|
|
d87448916f | ||
|
|
af4292658c | ||
|
|
f735bcd2ea | ||
|
|
66fb74af86 | ||
|
|
791eac30bb | ||
|
|
da4ad889d7 | ||
|
|
9ef111760c | ||
|
|
c2706c7d97 | ||
|
|
683b8768c4 | ||
|
|
6d9df237a8 | ||
|
|
220ae37aa7 | ||
|
|
d0d94d9239 | ||
|
|
aed3d1f147 | ||
|
|
d07bf08e45 | ||
|
|
f2183250d2 | ||
|
|
d2bb5ecfae | ||
|
|
02d0db0cf0 | ||
|
|
441005d5c3 | ||
|
|
eea8708549 | ||
|
|
5f3f9390aa | ||
|
|
1c5b0560ed | ||
|
|
1bfdf21b99 | ||
|
|
c4561cae38 | ||
|
|
efd522a38a | ||
|
|
55c35fa9c5 | ||
|
|
d42c27ae99 | ||
|
|
927be4a2c9 | ||
|
|
e44352abbd | ||
|
|
f9289f3c32 | ||
|
|
8ff06e5637 | ||
|
|
49e4633bca | ||
|
|
7ae7a7206c | ||
|
|
43d7e7babf | ||
|
|
463a51ce7c | ||
|
|
86e9d9d47f | ||
|
|
f99b72cd7c | ||
|
|
0dc2f3ff29 | ||
|
|
d0f8081101 | ||
|
|
84ebfb79d0 | ||
|
|
3c78d0ef46 | ||
|
|
e9a26fc995 | ||
|
|
b0794507f5 | ||
|
|
824e222e9d | ||
|
|
16a99d7d0f | ||
|
|
cda5c3d274 | ||
|
|
3d3791286d | ||
|
|
ad28bf2166 | ||
|
|
17bfa83204 | ||
|
|
6cd64e76ff | ||
|
|
a4bf50cf23 | ||
|
|
113baafd84 | ||
|
|
87c2b31821 | ||
|
|
8a6bc3625c | ||
|
|
bdfda8aced | ||
|
|
ca090436af | ||
|
|
55f85a04ea | ||
|
|
73f95cfa3b | ||
|
|
dbd3ded515 | ||
|
|
5399fac211 | ||
|
|
52e3127a34 | ||
|
|
3fb12c189b | ||
|
|
c4d5366b22 | ||
|
|
bd68e916cf | ||
|
|
7cadd9447b | ||
|
|
b649f178e0 | ||
|
|
7432c6477c | ||
|
|
c3aa6126fe | ||
|
|
0f3cf887c1 | ||
|
|
5cd311b69a | ||
|
|
057fda178f | ||
|
|
241b14a004 | ||
|
|
1f5e1e3d42 | ||
|
|
5727b0b0fe | ||
|
|
10b56089fa | ||
|
|
973df1ed5a | ||
|
|
8f681b83e8 | ||
|
|
2f38ed56f5 | ||
|
|
21501624e6 | ||
|
|
464530dacb | ||
|
|
0f2fc3dfec | ||
|
|
5cb71f1dc8 | ||
|
|
83e0a4d33e | ||
|
|
16502ea175 | ||
|
|
beee0407df | ||
|
|
3990b1ffbb | ||
|
|
1fb03708e3 | ||
|
|
e9ef254c6d | ||
|
|
d42719ee65 | ||
|
|
72ff489ea8 | ||
|
|
c9bf2dde0e | ||
|
|
613533d377 | ||
|
|
8568354718 | ||
|
|
1be6d32455 | ||
|
|
812a6e5005 | ||
|
|
34cc230b61 | ||
|
|
898a7c835f | ||
|
|
7766624bc5 | ||
|
|
2e8f73df38 | ||
|
|
6a419e060e | ||
|
|
43480ca735 | ||
|
|
efc42d77fd | ||
|
|
31e2523eca | ||
|
|
fbf4f40ab7 | ||
|
|
cbe203e720 | ||
|
|
09af118452 | ||
|
|
20d0c3d09b | ||
|
|
378a6684b0 | ||
|
|
d92891b223 | ||
|
|
1999ae09e6 | ||
|
|
aef86dc822 | ||
|
|
0fe48a0833 | ||
|
|
7bbf6dbf1c | ||
|
|
a3499c4628 | ||
|
|
689dc873b3 | ||
|
|
a0747d02e0 | ||
|
|
be5bd1e446 | ||
|
|
52ccfdec89 | ||
|
|
2c60591580 | ||
|
|
3cac6f69bd | ||
|
|
71ff71ccd2 | ||
|
|
da575ca262 | ||
|
|
5020566725 | ||
|
|
eb5915aa03 | ||
|
|
458ee7fe6c | ||
|
|
ea7eb18f36 | ||
|
|
18f5414411 | ||
|
|
a7ce6d85f4 | ||
|
|
2f348c660a | ||
|
|
599387934c | ||
|
|
04cea41111 | ||
|
|
dc3723306d | ||
|
|
d7fa572ab6 | ||
|
|
6140bc5b3b | ||
|
|
9f7780ec91 | ||
|
|
7c07d09ea4 | ||
|
|
13876ed523 | ||
|
|
abc7d0c7a5 | ||
|
|
074a36ea48 | ||
|
|
64e806dc27 | ||
|
|
bd0e9748b6 | ||
|
|
b21222b378 | ||
|
|
7e217db128 | ||
|
|
56c716d9fa | ||
|
|
14ecbd1fb9 | ||
|
|
a0242c4d6f | ||
|
|
4800b4a756 | ||
|
|
5b318d17d4 | ||
|
|
2f9be4f760 | ||
|
|
64777b6f30 | ||
|
|
7e1489353e | ||
|
|
c53306a497 | ||
|
|
83345579d0 | ||
|
|
2b4b9e0385 | ||
|
|
922349f550 | ||
|
|
d613f3fd04 | ||
|
|
4d8a47777e | ||
|
|
229a7ab1f7 | ||
|
|
3dabb7b53a | ||
|
|
abc3d6ce60 | ||
|
|
a529b654bc | ||
|
|
08d49bd1fd | ||
|
|
03435a2c66 | ||
|
|
66208d6840 | ||
|
|
5be9abb0fa | ||
|
|
8e504b5328 | ||
|
|
c21118257f | ||
|
|
4712b7ff68 | ||
|
|
4f305a8985 | ||
|
|
cd7d133ba3 | ||
|
|
2927a9ac31 | ||
|
|
695eaa77ca | ||
|
|
a29d21e194 | ||
|
|
cd20bd4ef2 | ||
|
|
0e3eb7204a | ||
|
|
b112ba0af4 | ||
|
|
70cfeb1fcf | ||
|
|
e6d990faa7 | ||
|
|
b45da7e360 | ||
|
|
3116562b58 | ||
|
|
693e40d385 | ||
|
|
ff186a8d09 | ||
|
|
3061771908 | ||
|
|
c681cc9bef | ||
|
|
3a80504427 | ||
|
|
9a1aa7bb2e | ||
|
|
98345f2e78 | ||
|
|
f29abe6238 | ||
|
|
8956d47bce | ||
|
|
dd0738d5f7 | ||
|
|
11d77d6011 | ||
|
|
a78cd2f18f | ||
|
|
e025c5857f | ||
|
|
6ef340daad | ||
|
|
a96e3c9163 | ||
|
|
c3c95053dc | ||
|
|
b27e94c712 | ||
|
|
279cf78aa5 | ||
|
|
8817adddf6 | ||
|
|
229c47cf16 | ||
|
|
1388f11508 | ||
|
|
e5e705350d | ||
|
|
4f81b0695d | ||
|
|
c2bfed6e1f | ||
|
|
97dc261fcd | ||
|
|
4ca93c2773 | ||
|
|
9c5b6532d3 | ||
|
|
f8b32584b4 | ||
|
|
889df8ca4d | ||
|
|
b998e09e10 | ||
|
|
9e0486a362 | ||
|
|
74037cec68 | ||
|
|
800db1b300 | ||
|
|
a40baa8c63 | ||
|
|
5cc06609c2 | ||
|
|
819e68b501 | ||
|
|
efa68aab83 | ||
|
|
3a696d366a | ||
|
|
e3e21b6164 | ||
|
|
9259663c76 | ||
|
|
26dd7faf05 | ||
|
|
10cc213933 | ||
|
|
1b5cb93761 | ||
|
|
4157c012fd | ||
|
|
8de1be4910 | ||
|
|
9515096349 | ||
|
|
4dd5617855 | ||
|
|
d0f9ffba73 | ||
|
|
01dc358842 | ||
|
|
925a1808e6 | ||
|
|
ac80f88727 | ||
|
|
3078247629 | ||
|
|
144c0084d2 | ||
|
|
bbdfb77a07 | ||
|
|
b6df9e2e8c | ||
|
|
48f15eb849 | ||
|
|
141642d40d | ||
|
|
def4a3a2ea | ||
|
|
fcb4d167e7 | ||
|
|
b5a9c1be47 | ||
|
|
c5137c6c45 | ||
|
|
297c2a965d | ||
|
|
5b5e7d9640 | ||
|
|
b876a4ada1 | ||
|
|
c02e0c63f2 | ||
|
|
7e064355ba | ||
|
|
788482fab2 | ||
|
|
16d94821b8 | ||
|
|
0f6ece6b8c | ||
|
|
5c4ab54c90 | ||
|
|
793e7392da | ||
|
|
5941568bbb | ||
|
|
1a9e1fde1d | ||
|
|
a9bbd1303e | ||
|
|
b0ed2b6f14 | ||
|
|
9194be4816 | ||
|
|
3e951eab4f | ||
|
|
cd7a198715 | ||
|
|
7c4d05a25e | ||
|
|
199fd0d491 | ||
|
|
32c0632526 | ||
|
|
19cca7f45d | ||
|
|
cd4b58674a | ||
|
|
191580a819 | ||
|
|
4a57861354 | ||
|
|
d96b817476 | ||
|
|
db6db8d860 | ||
|
|
fe405ba123 | ||
|
|
972a5f652f | ||
|
|
7a2c140524 | ||
|
|
3d717d68a9 | ||
|
|
13b6a47bef | ||
|
|
2a6caa47bd | ||
|
|
4f20d8640d | ||
|
|
5e8ae336a2 | ||
|
|
1ff73f4f00 | ||
|
|
7ce44ae1b1 | ||
|
|
1b847617b6 | ||
|
|
df53ec2954 | ||
|
|
7b4c32816e | ||
|
|
dbbfaef451 | ||
|
|
b4c07f1723 | ||
|
|
e983eb53d9 | ||
|
|
49867fdcf7 | ||
|
|
cc428d73ee | ||
|
|
940a1db0fc | ||
|
|
d70f29a408 | ||
|
|
52cb055520 | ||
|
|
8c406237a2 | ||
|
|
d036e282e5 | ||
|
|
39530cd8e8 | ||
|
|
8b58627608 | ||
|
|
c4e2d87e5c | ||
|
|
66b0378d38 | ||
|
|
22d7a36247 | ||
|
|
7d2eb2de66 | ||
|
|
4bba002c30 | ||
|
|
881a3344d4 | ||
|
|
d1562d33fb | ||
|
|
21ced66f22 | ||
|
|
2a2d86904d | ||
|
|
82d46f716b | ||
|
|
5e26810868 | ||
|
|
d590258371 | ||
|
|
697ef57cb8 | ||
|
|
93002dc8c3 | ||
|
|
357e0933ff | ||
|
|
baa1937d06 | ||
|
|
03e5662df9 | ||
|
|
e0711bdfc8 | ||
|
|
caca27fde3 | ||
|
|
b0d51033c6 | ||
|
|
088f9394fc | ||
|
|
a91361f971 | ||
|
|
a4f5be6ab9 | ||
|
|
dbbccbf1cd | ||
|
|
7d8f82b99d | ||
|
|
a70dc7b352 | ||
|
|
54df0df42b | ||
|
|
0bbb2598fd | ||
|
|
e10480b761 | ||
|
|
1343abbe50 | ||
|
|
4ba34cc827 | ||
|
|
fa37546139 | ||
|
|
42ece48ce3 | ||
|
|
112526a984 | ||
|
|
f97ab31f69 | ||
|
|
21dc1ecd6b | ||
|
|
c11adbe3e2 | ||
|
|
6078e9c207 | ||
|
|
450582dc43 | ||
|
|
ddec0e1be1 | ||
|
|
698154b24b | ||
|
|
9c87b0f67b | ||
|
|
85afe3d216 | ||
|
|
0773e5215f | ||
|
|
10f25fcc4e | ||
|
|
2ee90d6ea3 | ||
|
|
5db5323a1d | ||
|
|
0c74806245 | ||
|
|
5df84d7f50 | ||
|
|
d58de8fcf8 | ||
|
|
ed93d4b583 | ||
|
|
1ec1953eaa | ||
|
|
63e9c3933e | ||
|
|
85674c4d90 | ||
|
|
5a1d3b9bfc | ||
|
|
942570ed29 | ||
|
|
1b1620f633 | ||
|
|
e8e8d661e1 | ||
|
|
ba08ec7f5c | ||
|
|
fb0c98c21d | ||
|
|
c9f575c40c | ||
|
|
6c8bed7ecc | ||
|
|
3058eee48f | ||
|
|
d5a712f7ef | ||
|
|
83422f5ee6 | ||
|
|
51909a6a8f | ||
|
|
2f30797556 | ||
|
|
0b8f7d1661 | ||
|
|
52ee9d84b6 | ||
|
|
d9612b28b0 | ||
|
|
0034791493 | ||
|
|
80fed14a6b | ||
|
|
d457ada435 | ||
|
|
b41e5a9df5 | ||
|
|
0c8ace1bd4 | ||
|
|
3f800a068b | ||
|
|
7d490fe569 | ||
|
|
d6527122db | ||
|
|
3211140dec | ||
|
|
469352cd81 | ||
|
|
88400f6b7c | ||
|
|
f8c8a06d71 | ||
|
|
ebc1730fce | ||
|
|
c1cd1e813c | ||
|
|
e08a074474 | ||
|
|
2f819865bc | ||
|
|
3888f3041f | ||
|
|
bacb1b9720 | ||
|
|
e119e4fc18 | ||
|
|
569c4004f6 | ||
|
|
95932fa3f2 | ||
|
|
99402b77d1 | ||
|
|
f6fb2cd8e6 | ||
|
|
5c2cf59b41 | ||
|
|
a6d31dc260 | ||
|
|
b1fe2be963 | ||
|
|
872e50b635 | ||
|
|
bd73557a47 | ||
|
|
9b6e8ab3bc | ||
|
|
c95bab70c2 | ||
|
|
fe0742e278 | ||
|
|
ded57d3b24 | ||
|
|
c30abaea22 | ||
|
|
d2c4b7cad1 | ||
|
|
59d737696a | ||
|
|
22de3214f1 | ||
|
|
cf880f992f | ||
|
|
195adfb04a | ||
|
|
aee4cdcb72 | ||
|
|
87af60cc03 | ||
|
|
3d8dd39995 | ||
|
|
65687beecc | ||
|
|
62aa859737 | ||
|
|
d8c2d369aa | ||
|
|
a4e4926aeb | ||
|
|
35cd76e562 | ||
|
|
266bbe837d | ||
|
|
caf785a938 | ||
|
|
9bc3e755df | ||
|
|
638a7ac11d | ||
|
|
4e49c8db50 | ||
|
|
210f65b4db | ||
|
|
1b6482126f | ||
|
|
96f9c1a55d | ||
|
|
731460b20d | ||
|
|
1537d46b1d | ||
|
|
632def158d | ||
|
|
39271a67e2 | ||
|
|
9e25c4f386 | ||
|
|
dd58a4ac7f | ||
|
|
b9c3567baa | ||
|
|
108937789a | ||
|
|
e651745a7e | ||
|
|
699debb2b8 | ||
|
|
3e08dc7f8c | ||
|
|
6928b48781 | ||
|
|
02886350ff | ||
|
|
b3672f8246 | ||
|
|
6091b4a8e8 | ||
|
|
82ddcbd180 | ||
|
|
8aa7aafa3b | ||
|
|
183cb4b26a | ||
|
|
3a7377c6e2 | ||
|
|
1529f58c33 | ||
|
|
95af5421d1 | ||
|
|
feb39404db | ||
|
|
15b3100c63 | ||
|
|
f7ef7d106d | ||
|
|
0dbbcc5595 | ||
|
|
816f8d069d | ||
|
|
72c31622cd | ||
|
|
6959461e3f | ||
|
|
103472ac77 | ||
|
|
2ebf99ff8f | ||
|
|
c13e492bbf | ||
|
|
63476a2351 | ||
|
|
782252c059 | ||
|
|
e86978a1ff | ||
|
|
84cfd11953 | ||
|
|
9a43e136f6 | ||
|
|
e9cff26fa0 | ||
|
|
3d32bca2b3 | ||
|
|
4021feaf38 | ||
|
|
306ec74356 | ||
|
|
2764a1c4b6 | ||
|
|
4b2d2a4f55 | ||
|
|
eba2bd05b8 | ||
|
|
84a1b28261 | ||
|
|
ff3b9df41e | ||
|
|
a0901914ac | ||
|
|
f1272947dd | ||
|
|
e5041bfd30 | ||
|
|
3d7cc74feb | ||
|
|
d007202783 | ||
|
|
38e92a705d | ||
|
|
74cc63833a | ||
|
|
425320bbb5 | ||
|
|
499352ad8a | ||
|
|
26d577d7ae | ||
|
|
622c48a94b | ||
|
|
e1a87a05b1 | ||
|
|
2148317282 | ||
|
|
5f9c6c8346 | ||
|
|
2e30371086 | ||
|
|
57db5b83d4 | ||
|
|
66659bb293 | ||
|
|
579d4f3170 | ||
|
|
bb56548603 | ||
|
|
3cab18713a | ||
|
|
fb94dae43a | ||
|
|
f694846eae | ||
|
|
322ab50138 | ||
|
|
435efd2bc5 | ||
|
|
5501a5937e | ||
|
|
80a6808a82 | ||
|
|
987bd70312 | ||
|
|
feb22e62c1 | ||
|
|
e7d4c77a6d | ||
|
|
628e32464d | ||
|
|
dc82043254 | ||
|
|
997e9d58a8 | ||
|
|
8a3f1706fe | ||
|
|
12cbe4d534 | ||
|
|
9f21931201 | ||
|
|
09351e1910 | ||
|
|
1c2ea5a407 |
@@ -7,4 +7,4 @@
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
}
|
||||
7
.github/actions/nhost-cli/action.yaml
vendored
7
.github/actions/nhost-cli/action.yaml
vendored
@@ -35,8 +35,11 @@ runs:
|
||||
fi
|
||||
- name: Install Nhost CLI
|
||||
if: ${{ steps.check-nhost-cli.outputs.installed == 'false' }}
|
||||
shell: bash
|
||||
run: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 3
|
||||
max_attempts: 10
|
||||
command: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
|
||||
- name: Set custom configuration
|
||||
if: ${{ inputs.config }}
|
||||
shell: bash
|
||||
|
||||
7
.github/labeler.yml
vendored
7
.github/labeler.yml
vendored
@@ -12,11 +12,14 @@ examples:
|
||||
sdk:
|
||||
- packages/**/*
|
||||
|
||||
integrations:
|
||||
- integrations/**/*
|
||||
|
||||
react:
|
||||
- '{packages,examples}/*react*/**/*'
|
||||
- '{packages,examples,integrations}/*react*/**/*'
|
||||
|
||||
nextjs:
|
||||
- '{packages,examples}/*next*/**/*'
|
||||
|
||||
vue:
|
||||
- '{packages,examples}/*vue*/**/*'
|
||||
- '{packages,examples,integrations}/*vue*/**/*'
|
||||
|
||||
9
.github/renovate.json
vendored
9
.github/renovate.json
vendored
@@ -8,9 +8,16 @@
|
||||
},
|
||||
"ignoreDeps": [
|
||||
"pnpm",
|
||||
"node"
|
||||
"node",
|
||||
"@types/node"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"enabledManagers": [
|
||||
"npm",
|
||||
"dockerfile",
|
||||
"docker-compose",
|
||||
"github-actions"
|
||||
]
|
||||
}
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
BUILD: 'all'
|
||||
- name: Check if the pnpm lockfile changed
|
||||
id: changed-lockfile
|
||||
uses: tj-actions/changed-files@v34
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: pnpm-lock.yaml
|
||||
# * Determine a pnpm filter argument for packages that have been modified.
|
||||
|
||||
15
.github/workflows/contributors.yaml
vendored
15
.github/workflows/contributors.yaml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Add contributors
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
contrib-readme-job:
|
||||
runs-on: ubuntu-latest
|
||||
name: A job to automate contrib in readme
|
||||
steps:
|
||||
- name: Contribute List
|
||||
uses: akhilmhdh/contributors-readme-action@v2.3.6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
29
.github/workflows/renovate.yaml
vendored
29
.github/workflows/renovate.yaml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
- 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
|
||||
@@ -61,8 +62,28 @@ jobs:
|
||||
|
||||
${{ github.event.pull_request.title }}
|
||||
EOF
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
if: steps.bumps.outputs.result != ''
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit_message: ${{ github.event.pull_request.title }}
|
||||
branch: main
|
||||
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 }}
|
||||
|
||||
456
README.md
456
README.md
@@ -101,14 +101,17 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
- [JavaScript/TypeScript SDK](https://docs.nhost.io/reference/javascript)
|
||||
- [Dart and Flutter SDK](https://github.com/nhost/nhost-dart)
|
||||
- [Nhost React](https://docs.nhost.io/reference/react)
|
||||
- [Nhost Next.js](https://docs.nhost.io/reference/nextjs)
|
||||
- [Nhost Vue](https://docs.nhost.io/reference/vue)
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript)
|
||||
- [Dart and Flutter](https://github.com/nhost/nhost-dart)
|
||||
- [React](https://docs.nhost.io/reference/react)
|
||||
- [Next.js](https://docs.nhost.io/reference/nextjs)
|
||||
- [Vue](https://docs.nhost.io/reference/vue)
|
||||
|
||||
## Integrations
|
||||
|
||||
- [Apollo](./integrations/apollo#nhostapollo)
|
||||
- [React Apollo](./integrations/react-apollo#nhostreact-apollo)
|
||||
- [React URQL](./integrations/react-urql#nhostreact-urql)
|
||||
- [Stripe GraphQL API](./integrations/stripe-graphql-js#nhoststripe-graphql-js)
|
||||
- [Google Translation GraphQL API](./integrations/google-translation#nhostgoogle-translation)
|
||||
|
||||
@@ -127,8 +130,8 @@ Also, follow Nhost on [GitHub Discussions](https://github.com/nhost/nhost/discus
|
||||
|
||||
This repository, and most of our other open source projects, are licensed under the MIT license.
|
||||
|
||||
<a href="https://runacap.com/ross-index/q1-2022/" target="_blank" rel="noopener">
|
||||
<img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2022/06/ROSS_badge_black_Q1_2022.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q1 2022 | Runa Capital" width="260" height="56" />
|
||||
<a href="https://runacap.com/ross-index/" target="_blank" rel="noopener" >
|
||||
<img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2022/06/ROSS_black_edition_badge.svg" alt="ROSS Index - Fastest Growing Open-Source Startups | Runa Capital" width="260" height="56" />
|
||||
</a>
|
||||
|
||||
### How to contribute
|
||||
@@ -141,437 +144,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
|
||||
### Contributors
|
||||
|
||||
<!-- readme: contributors -start -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/plmercereau">
|
||||
<img src="https://avatars.githubusercontent.com/u/24897252?v=4" width="100;" alt="plmercereau"/>
|
||||
<br />
|
||||
<sub><b>Pilou</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/elitan">
|
||||
<img src="https://avatars.githubusercontent.com/u/331818?v=4" width="100;" alt="elitan"/>
|
||||
<br />
|
||||
<sub><b>Johan Eliasson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/szilarddoro">
|
||||
<img src="https://avatars.githubusercontent.com/u/310881?v=4" width="100;" alt="szilarddoro"/>
|
||||
<br />
|
||||
<sub><b>Szilárd Dóró</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nunopato">
|
||||
<img src="https://avatars.githubusercontent.com/u/1523504?v=4" width="100;" alt="nunopato"/>
|
||||
<br />
|
||||
<sub><b>Nuno Pato</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/gdangelo">
|
||||
<img src="https://avatars.githubusercontent.com/u/4352286?v=4" width="100;" alt="gdangelo"/>
|
||||
<br />
|
||||
<sub><b>Grégory D'Angelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ejkkan">
|
||||
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
|
||||
<br />
|
||||
<sub><b>Erik Magnusson</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/guicurcio">
|
||||
<img src="https://avatars.githubusercontent.com/u/20285232?v=4" width="100;" alt="guicurcio"/>
|
||||
<br />
|
||||
<sub><b>Guido Curcio</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subatuba21">
|
||||
<img src="https://avatars.githubusercontent.com/u/34824571?v=4" width="100;" alt="subatuba21"/>
|
||||
<br />
|
||||
<sub><b>Subha Das</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/sebagudelo">
|
||||
<img src="https://avatars.githubusercontent.com/u/43288271?v=4" width="100;" alt="sebagudelo"/>
|
||||
<br />
|
||||
<sub><b>Sebagudelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mrinalwahal">
|
||||
<img src="https://avatars.githubusercontent.com/u/9859731?v=4" width="100;" alt="mrinalwahal"/>
|
||||
<br />
|
||||
<sub><b>Mrinal Wahal</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/timpratim">
|
||||
<img src="https://avatars.githubusercontent.com/u/32492961?v=4" width="100;" alt="timpratim"/>
|
||||
<br />
|
||||
<sub><b>Pratim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/chrtze">
|
||||
<img src="https://avatars.githubusercontent.com/u/3797215?v=4" width="100;" alt="chrtze"/>
|
||||
<br />
|
||||
<sub><b>Christopher Möller</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/GavanWilhite">
|
||||
<img src="https://avatars.githubusercontent.com/u/2085119?v=4" width="100;" alt="GavanWilhite"/>
|
||||
<br />
|
||||
<sub><b>Gavan Wilhite</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/FuzzyReason">
|
||||
<img src="https://avatars.githubusercontent.com/u/62517920?v=4" width="100;" alt="FuzzyReason"/>
|
||||
<br />
|
||||
<sub><b>Vadim Smirnov</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/macmac49">
|
||||
<img src="https://avatars.githubusercontent.com/u/831190?v=4" width="100;" alt="macmac49"/>
|
||||
<br />
|
||||
<sub><b>Macmac49</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subhendukundu">
|
||||
<img src="https://avatars.githubusercontent.com/u/20059141?v=4" width="100;" alt="subhendukundu"/>
|
||||
<br />
|
||||
<sub><b>Subhendu Kundu</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/badgifter">
|
||||
<img src="https://avatars.githubusercontent.com/u/50094885?v=4" width="100;" alt="badgifter"/>
|
||||
<br />
|
||||
<sub><b>Bad Gifter</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/heygambo">
|
||||
<img src="https://avatars.githubusercontent.com/u/449438?v=4" width="100;" alt="heygambo"/>
|
||||
<br />
|
||||
<sub><b>Christian Gambardella</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dbarrosop">
|
||||
<img src="https://avatars.githubusercontent.com/u/6246622?v=4" width="100;" alt="dbarrosop"/>
|
||||
<br />
|
||||
<sub><b>David Barroso</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/hajek-raven">
|
||||
<img src="https://avatars.githubusercontent.com/u/7288737?v=4" width="100;" alt="hajek-raven"/>
|
||||
<br />
|
||||
<sub><b>Filip Hájek</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MelodicCrypter">
|
||||
<img src="https://avatars.githubusercontent.com/u/18341500?v=4" width="100;" alt="MelodicCrypter"/>
|
||||
<br />
|
||||
<sub><b>Hugh Caluscusin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jerryjappinen">
|
||||
<img src="https://avatars.githubusercontent.com/u/1101002?v=4" width="100;" alt="jerryjappinen"/>
|
||||
<br />
|
||||
<sub><b>Jerry Jäppinen</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/wollerman">
|
||||
<img src="https://avatars.githubusercontent.com/u/1610241?v=4" width="100;" alt="wollerman"/>
|
||||
<br />
|
||||
<sub><b>Matt Wollerman</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mdp18">
|
||||
<img src="https://avatars.githubusercontent.com/u/11698527?v=4" width="100;" alt="mdp18"/>
|
||||
<br />
|
||||
<sub><b>Max</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mustafa-hanif">
|
||||
<img src="https://avatars.githubusercontent.com/u/30019262?v=4" width="100;" alt="mustafa-hanif"/>
|
||||
<br />
|
||||
<sub><b>Mustafa Hanif</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nbourdin">
|
||||
<img src="https://avatars.githubusercontent.com/u/5602476?v=4" width="100;" alt="nbourdin"/>
|
||||
<br />
|
||||
<sub><b>Nicolas Bourdin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/piromsurang">
|
||||
<img src="https://avatars.githubusercontent.com/u/17776837?v=4" width="100;" alt="piromsurang"/>
|
||||
<br />
|
||||
<sub><b>Piromsurang Rungserichai</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Savinvadim1312">
|
||||
<img src="https://avatars.githubusercontent.com/u/16936043?v=4" width="100;" alt="Savinvadim1312"/>
|
||||
<br />
|
||||
<sub><b>Savin Vadim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Svarto">
|
||||
<img src="https://avatars.githubusercontent.com/u/24279217?v=4" width="100;" alt="Svarto"/>
|
||||
<br />
|
||||
<sub><b>Svarto</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/muttenzer">
|
||||
<img src="https://avatars.githubusercontent.com/u/49474412?v=4" width="100;" alt="muttenzer"/>
|
||||
<br />
|
||||
<sub><b>Muttenzer</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alexander-mart">
|
||||
<img src="https://avatars.githubusercontent.com/u/14993551?v=4" width="100;" alt="alexander-mart"/>
|
||||
<br />
|
||||
<sub><b>Alexander Mart</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ahmic">
|
||||
<img src="https://avatars.githubusercontent.com/u/13452362?v=4" width="100;" alt="ahmic"/>
|
||||
<br />
|
||||
<sub><b>Amir Ahmic</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/akd-io">
|
||||
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
||||
<br />
|
||||
<sub><b>Anders Kjær Damgaard</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Sonichigo">
|
||||
<img src="https://avatars.githubusercontent.com/u/53110238?v=4" width="100;" alt="Sonichigo"/>
|
||||
<br />
|
||||
<sub><b>Animesh Pathak</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/chrisli-03">
|
||||
<img src="https://avatars.githubusercontent.com/u/11177048?v=4" width="100;" alt="chrisli-03"/>
|
||||
<br />
|
||||
<sub><b>Chris</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/massless">
|
||||
<img src="https://avatars.githubusercontent.com/u/44389?v=4" width="100;" alt="massless"/>
|
||||
<br />
|
||||
<sub><b>Chris Wetherell</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rustyb">
|
||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||
<br />
|
||||
<sub><b>Colin Broderick</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/daguitosama">
|
||||
<img src="https://avatars.githubusercontent.com/u/34744883?v=4" width="100;" alt="daguitosama"/>
|
||||
<br />
|
||||
<sub><b>Dago</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dminkovsky">
|
||||
<img src="https://avatars.githubusercontent.com/u/218725?v=4" width="100;" alt="dminkovsky"/>
|
||||
<br />
|
||||
<sub><b>Dmitry Minkovsky</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dohomi">
|
||||
<img src="https://avatars.githubusercontent.com/u/489221?v=4" width="100;" alt="dohomi"/>
|
||||
<br />
|
||||
<sub><b>Dominic Garms</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/gaurav1999">
|
||||
<img src="https://avatars.githubusercontent.com/u/20752142?v=4" width="100;" alt="gaurav1999"/>
|
||||
<br />
|
||||
<sub><b>Gaurav Agrawal</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alveshelio">
|
||||
<img src="https://avatars.githubusercontent.com/u/8176422?v=4" width="100;" alt="alveshelio"/>
|
||||
<br />
|
||||
<sub><b>Helio Alves</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nkhdo">
|
||||
<img src="https://avatars.githubusercontent.com/u/26102306?v=4" width="100;" alt="nkhdo"/>
|
||||
<br />
|
||||
<sub><b>Hoang Do</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/iangabrielsanchez">
|
||||
<img src="https://avatars.githubusercontent.com/u/9511946?v=4" width="100;" alt="iangabrielsanchez"/>
|
||||
<br />
|
||||
<sub><b>Ian Gabriel Sanchez</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/eltociear">
|
||||
<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/>
|
||||
<br />
|
||||
<sub><b>Ikko Ashimine</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jladuval">
|
||||
<img src="https://avatars.githubusercontent.com/u/1935359?v=4" width="100;" alt="jladuval"/>
|
||||
<br />
|
||||
<sub><b>Jacob Duval</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/leothorp">
|
||||
<img src="https://avatars.githubusercontent.com/u/12928449?v=4" width="100;" alt="leothorp"/>
|
||||
<br />
|
||||
<sub><b>Leo Thorp</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/LucasBois1">
|
||||
<img src="https://avatars.githubusercontent.com/u/44686060?v=4" width="100;" alt="LucasBois1"/>
|
||||
<br />
|
||||
<sub><b>Lucas Bois</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MarcelloTheArcane">
|
||||
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
|
||||
<br />
|
||||
<sub><b>Max Reynolds</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nachoaldamav">
|
||||
<img src="https://avatars.githubusercontent.com/u/22749943?v=4" width="100;" alt="nachoaldamav"/>
|
||||
<br />
|
||||
<sub><b>Nacho Aldama</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ghoshnirmalya">
|
||||
<img src="https://avatars.githubusercontent.com/u/6391763?v=4" width="100;" alt="ghoshnirmalya"/>
|
||||
<br />
|
||||
<sub><b>Nirmalya Ghosh</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/quentin-decre">
|
||||
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
|
||||
<br />
|
||||
<sub><b>Quentin Decré</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/elephant3">
|
||||
<img src="https://avatars.githubusercontent.com/u/48279149?v=4" width="100;" alt="elephant3"/>
|
||||
<br />
|
||||
<sub><b>Siarhei Lipchyk</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/altschuler">
|
||||
<img src="https://avatars.githubusercontent.com/u/956928?v=4" width="100;" alt="altschuler"/>
|
||||
<br />
|
||||
<sub><b>Simon Altschuler</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/atapas">
|
||||
<img src="https://avatars.githubusercontent.com/u/3633137?v=4" width="100;" alt="atapas"/>
|
||||
<br />
|
||||
<sub><b>Tapas Adhikary</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/uulwake">
|
||||
<img src="https://avatars.githubusercontent.com/u/22399181?v=4" width="100;" alt="uulwake"/>
|
||||
<br />
|
||||
<sub><b>Ulrich Wake</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/kwarabei">
|
||||
<img src="https://avatars.githubusercontent.com/u/102731455?v=4" width="100;" alt="kwarabei"/>
|
||||
<br />
|
||||
<sub><b>Vadim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/TheRedLancer">
|
||||
<img src="https://avatars.githubusercontent.com/u/58493767?v=4" width="100;" alt="TheRedLancer"/>
|
||||
<br />
|
||||
<sub><b>Zach Burnaby</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/komninoschat">
|
||||
<img src="https://avatars.githubusercontent.com/u/29049104?v=4" width="100;" alt="komninoschat"/>
|
||||
<br />
|
||||
<sub><b>Komninos</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/meesvandongen">
|
||||
<img src="https://avatars.githubusercontent.com/u/35409045?v=4" width="100;" alt="meesvandongen"/>
|
||||
<br />
|
||||
<sub><b>Meesvandongen</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<!-- readme: contributors -end -->
|
||||
<a href="https://github.com/nhost/nhost/graphs/contributors">
|
||||
<p align="center">
|
||||
<img width="720" src="https://contrib.rocks/image?repo=nhost/nhost" alt="A table of avatars from the project's contributors" />
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -19,7 +19,9 @@ module.exports = {
|
||||
'*.spec.ts',
|
||||
'*.spec.tsx',
|
||||
'tests/**/*.ts',
|
||||
'tests/**/*.d.ts'
|
||||
'tests/**/*.d.ts',
|
||||
'e2e/**/*.ts',
|
||||
'e2e/**/*.d.ts'
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'cypress'],
|
||||
extends: ['plugin:cypress/recommended'],
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
module.exports = {
|
||||
'(packages|integrations)/(docgen|hasura-auth-js|hasura-storage-js|nextjs|nhost-js|react|core|vue)/src/**/*.{js,ts,jsx,tsx}':
|
||||
['pnpm docgen', 'git add docs'],
|
||||
'(nhost-cloud.yaml|**/nhost/config.yaml)': () => [
|
||||
'pnpm sync-versions',
|
||||
"git add ':(glob)**/nhost/config.yaml'"
|
||||
|
||||
@@ -28,22 +28,44 @@
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types", "**/*/dist", "**/*/build", "**/*/.next", "**/*/umd"
|
||||
"./node_modules/@types",
|
||||
"**/*/dist",
|
||||
"**/*/build",
|
||||
"**/*/.next",
|
||||
"**/*/umd"
|
||||
],
|
||||
"paths": {
|
||||
"@nhost/apollo": ["../packages/apollo/src/index.ts"],
|
||||
"@nhost/core": ["../packages/core/src/index.ts"],
|
||||
"@nhost/docgen": ["../packages/docgen/src/index.ts"],
|
||||
"@nhost/hasura-auth-js": ["../packages/hasura-auth-js/src/index.ts"],
|
||||
"@nhost/hasura-storage-js": ["../packages/hasura-storage-js/src/index.ts"],
|
||||
"@nhost/nextjs": ["../packages/nextjs/src/index.ts"],
|
||||
"@nhost/nhost-js": ["../packages/nhost-js/src/index.ts"],
|
||||
"@nhost/react": ["../packages/react/src/index.ts"],
|
||||
"@nhost/react-apollo": ["../packages/react-apollo/src/index.ts"],
|
||||
"@nhost/react-auth": ["../packages/react-auth/src/index.ts"],
|
||||
"@nhost/vue": ["../packages/vue/src/index.ts"]
|
||||
"@nhost/apollo": [
|
||||
"../integrations/apollo/src/index.ts"
|
||||
],
|
||||
"@nhost/docgen": [
|
||||
"../packages/docgen/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-auth-js": [
|
||||
"../packages/hasura-auth-js/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-storage-js": [
|
||||
"../packages/hasura-storage-js/src/index.ts"
|
||||
],
|
||||
"@nhost/nextjs": [
|
||||
"../packages/nextjs/src/index.ts"
|
||||
],
|
||||
"@nhost/nhost-js": [
|
||||
"../packages/nhost-js/src/index.ts"
|
||||
],
|
||||
"@nhost/react": [
|
||||
"../packages/react/src/index.ts"
|
||||
],
|
||||
"@nhost/react-apollo": [
|
||||
"../integrations/react-apollo/src/index.ts"
|
||||
],
|
||||
"@nhost/vue": [
|
||||
"../packages/vue/src/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
@@ -18,7 +19,9 @@ export default defineConfig({
|
||||
tsconfigPaths(),
|
||||
dts({
|
||||
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
|
||||
entryRoot: 'src'
|
||||
entryRoot: 'src',
|
||||
// Was defaulting to true until version 1.7
|
||||
skipDiagnostics: true
|
||||
})
|
||||
],
|
||||
test: {
|
||||
@@ -41,6 +44,12 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
external: (id) => deps.some((dep) => id.startsWith(dep)),
|
||||
plugins: [
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
'exports.hasOwnProperty(': 'Object.prototype.hasOwnProperty.call(exports,'
|
||||
})
|
||||
],
|
||||
output: {
|
||||
globals: {
|
||||
graphql: 'graphql',
|
||||
|
||||
@@ -6,6 +6,7 @@ module.exports = {
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-addon-next-router',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
@@ -38,4 +39,10 @@ module.exports = {
|
||||
},
|
||||
};
|
||||
},
|
||||
env: (config) => ({
|
||||
...config,
|
||||
NEXT_PUBLIC_ENV: 'dev',
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: 'http://localhost:1337',
|
||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -2,9 +2,25 @@ import '@fontsource/inter';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Buffer } from 'buffer';
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context';
|
||||
import '../src/styles/globals.css';
|
||||
import defaultTheme from '../src/theme/default';
|
||||
|
||||
global.Buffer = Buffer;
|
||||
|
||||
initialize({ onUnhandledRequest: 'bypass' });
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const parameters = {
|
||||
nextRouter: {
|
||||
Provider: RouterContext.Provider,
|
||||
isReady: true,
|
||||
},
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
@@ -14,11 +30,25 @@ export const parameters = {
|
||||
},
|
||||
};
|
||||
|
||||
export const withMuiTheme = (Story) => (
|
||||
<ThemeProvider theme={defaultTheme}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export const decorators = [withMuiTheme];
|
||||
export const decorators = [
|
||||
(Story) => (
|
||||
<ThemeProvider theme={defaultTheme}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostApolloProvider
|
||||
fetchPolicy="cache-first"
|
||||
graphqlUrl="http://localhost:1337/v1/graphql"
|
||||
>
|
||||
<Story />
|
||||
</NhostApolloProvider>
|
||||
),
|
||||
mswDecorator,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,158 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e146d32e: chore(deps): update dependency @types/react to v18.0.27
|
||||
- 59347fcd: correct allowed role name
|
||||
- 5b65cac9: updated authentication documentation
|
||||
- 963f9b5e: feat(dashboard): include project info in feedback
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ed4c7801: chore(dashboard): remove Functions section
|
||||
|
||||
## 0.9.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4e2f8ccd: fix(dashboard): don't break Auth page in local mode
|
||||
|
||||
## 0.9.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 31abbe5f: fix(dashboard): enable toggle when settings are filled in
|
||||
|
||||
## 0.9.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5bdd31ad: chore(dashboard): list fewer images per page on the Storage page
|
||||
- 5121851c: fix(dashboard): don't throw validation error for valid permission rules
|
||||
|
||||
## 0.9.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c126b20d: fix(dashboard): correct redeployment button
|
||||
|
||||
## 0.9.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 36c3519c: feat(dashboard): retrigger deployments
|
||||
|
||||
## 0.9.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 200e9f77: chore(deps): update dependency @types/react-dom to v18.0.10
|
||||
- Updated dependencies [200e9f77]
|
||||
- @nhost/nextjs@1.13.2
|
||||
- @nhost/react-apollo@4.13.2
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dbd3ded5: fix(dashboard): workspaces creation, new form, correct redirects.
|
||||
|
||||
## 0.9.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 85f0f943: fix(dashboard): don't break the table creation process
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d42c27ae]
|
||||
- Updated dependencies [927be4a2]
|
||||
- @nhost/nextjs@1.13.1
|
||||
- @nhost/react-apollo@4.13.1
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d0f80811: fix(dashboard): don't show error when signing out the user
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d92891b2: feat(dashboard): add Permission Editor to the Database section
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3d379128: fix(dashboard): create new user
|
||||
- @nhost/react-apollo@4.13.0
|
||||
- @nhost/nextjs@1.13.0
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7cadd944: fix(dashboard): display Twitter provider settings
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9a1aa7bb: add functions to the log dashboard
|
||||
- f29abe62: feat(dashboard): Users Management v2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7766624b: feat(dashboard): add JWT secret editor modal
|
||||
- @nhost/react-apollo@4.12.1
|
||||
- @nhost/nextjs@1.12.1
|
||||
|
||||
## 0.7.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dd0738d5: fix(dashboard): provisioning status polling
|
||||
|
||||
## 0.7.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b21222b3: chore(deps): update dependency @types/node to v16
|
||||
- 9e0486a3: fix(dashboard): close modals when navigating
|
||||
- Updated dependencies [b21222b3]
|
||||
- Updated dependencies [65687bee]
|
||||
- Updated dependencies [54df0df4]
|
||||
- @nhost/nextjs@1.12.0
|
||||
- @nhost/react-apollo@4.12.0
|
||||
|
||||
## 0.7.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d6527122: fix(dashboard): use correct service URLs
|
||||
|
||||
## 0.7.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [57db5b83]
|
||||
- @nhost/nextjs@1.11.0
|
||||
- @nhost/nhost-js@1.7.0
|
||||
- @nhost/react@0.17.0
|
||||
- @nhost/react-apollo@4.11.0
|
||||
|
||||
## 0.7.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a6d31dc2: fix(dashboard): don't break the UI when project is not loaded yet
|
||||
|
||||
## 0.7.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN yarn global add turbo@1
|
||||
COPY . .
|
||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Nhost Dashboard
|
||||
|
||||
This is the Nhost Dashboard, a web application that allows you to manage your Nhost project.
|
||||
This is the Nhost Dashboard, a web application that allows you to manage your Nhost projects.
|
||||
To get started, you need to have an Nhost project. If you don't have one, you can [create a project here](https://app.nhost.io).
|
||||
|
||||
```bash
|
||||
@@ -37,6 +37,14 @@ NEXT_PUBLIC_ENV=dev
|
||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
```
|
||||
|
||||
### Storybook
|
||||
|
||||
Components are documented using [Storybook](https://storybook.js.org/). To run Storybook, run the following command:
|
||||
|
||||
```bash
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
### Full list of environment variables
|
||||
|
||||
| Name | Description |
|
||||
|
||||
@@ -66,6 +66,11 @@ module.exports = withBundleAnalyzer({
|
||||
destination: '/:workspaceSlug/:appSlug/settings/environment-variables',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/:workspaceSlug/:appSlug/users/:userId',
|
||||
destination: '/:workspaceSlug/:appSlug/users?userId=:userId',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.7.8",
|
||||
"version": "0.10.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -8,16 +8,16 @@
|
||||
"build": "next build --no-lint",
|
||||
"analyze": "ANALYZE=true pnpm build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings 3",
|
||||
"lint": "next lint --max-warnings 2",
|
||||
"test": "vitest",
|
||||
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
|
||||
"nhost:dev": "nhost dev -d",
|
||||
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook": "start-storybook -p 6006 -s public",
|
||||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.2",
|
||||
"@apollo/client": "^3.7.3",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@emotion/cache": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
@@ -34,10 +34,7 @@
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/system": "^5.10.14",
|
||||
"@mui/x-date-pickers": "^5.0.8",
|
||||
"@nhost/core": "workspace:*",
|
||||
"@nhost/nextjs": "workspace:*",
|
||||
"@nhost/nhost-js": "workspace:*",
|
||||
"@nhost/react": "workspace:*",
|
||||
"@nhost/react-apollo": "workspace:*",
|
||||
"@segment/snippet": "^4.15.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
@@ -91,31 +88,31 @@
|
||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||
"@next/bundle-analyzer": "^12.3.1",
|
||||
"@storybook/addon-actions": "^6.5.13",
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
"@storybook/addon-interactions": "^6.5.13",
|
||||
"@storybook/addon-links": "^6.5.13",
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.14",
|
||||
"@storybook/addon-interactions": "^6.5.14",
|
||||
"@storybook/addon-links": "^6.5.14",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/builder-webpack5": "^6.5.14",
|
||||
"@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/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.0.25",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react": "18.0.27",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@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": "^2.2.0",
|
||||
"@vitest/coverage-c8": "^0.25.2",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"@vitest/coverage-c8": "^0.27.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
@@ -129,22 +126,24 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jsdom": "^20.0.3",
|
||||
"jsdom": "^21.0.0",
|
||||
"lint-staged": ">=13",
|
||||
"msw": "^0.49.0",
|
||||
"msw-storybook-addon": "^1.6.3",
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"storybook-addon-next-router": "^4.0.1",
|
||||
"tailwindcss": "^3.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.4",
|
||||
"vite-tsconfig-paths": "^3.6.0",
|
||||
"vitest": "^0.25.2",
|
||||
"vite": "^4.0.2",
|
||||
"vite-tsconfig-paths": "^4.0.3",
|
||||
"vitest": "^0.27.0",
|
||||
"webpack": "^5.75.0"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -158,5 +157,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
303
dashboard/public/mockServiceWorker.js
Normal file
303
dashboard/public/mockServiceWorker.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker (0.49.0).
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
|
||||
const activeClientIds = new Set()
|
||||
|
||||
self.addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener('message', async function (event) {
|
||||
const clientId = event.source.id
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: INTEGRITY_CHECKSUM,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_DEACTIVATE': {
|
||||
activeClientIds.delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const { request } = event
|
||||
const accept = request.headers.get('accept') || ''
|
||||
|
||||
// Bypass server-sent events.
|
||||
if (accept.includes('text/event-stream')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been deleted (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID.
|
||||
const requestId = Math.random().toString(16).slice(2)
|
||||
|
||||
event.respondWith(
|
||||
handleRequest(event, requestId).catch((error) => {
|
||||
if (error.name === 'NetworkError') {
|
||||
console.warn(
|
||||
'[MSW] Successfully emulated a network error for the "%s %s" request.',
|
||||
request.method,
|
||||
request.url,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// At this point, any exception indicates an issue with the original request/response.
|
||||
console.error(
|
||||
`\
|
||||
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
|
||||
request.method,
|
||||
request.url,
|
||||
`${error.name}: ${error.message}`,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
;(async function () {
|
||||
const clonedResponse = response.clone()
|
||||
sendToClient(client, {
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
type: clonedResponse.type,
|
||||
ok: clonedResponse.ok,
|
||||
status: clonedResponse.status,
|
||||
statusText: clonedResponse.statusText,
|
||||
body:
|
||||
clonedResponse.body === null ? null : await clonedResponse.text(),
|
||||
headers: Object.fromEntries(clonedResponse.headers.entries()),
|
||||
redirected: clonedResponse.redirected,
|
||||
},
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Resolve the main client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
const clonedRequest = request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const headers = Object.fromEntries(clonedRequest.headers.entries())
|
||||
|
||||
// Remove MSW-specific request headers so the bypassed requests
|
||||
// comply with the server's CORS preflight check.
|
||||
// Operate with the headers as an object because request "Headers"
|
||||
// are immutable.
|
||||
delete headers['x-msw-bypass']
|
||||
|
||||
return fetch(clonedRequest, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass requests with the explicit bypass header.
|
||||
// Such requests can be issued by "ctx.fetch()".
|
||||
if (request.headers.get('x-msw-bypass') === 'true') {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const clientMessage = await sendToClient(client, {
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
mode: request.mode,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.text(),
|
||||
bodyUsed: request.bodyUsed,
|
||||
keepalive: request.keepalive,
|
||||
},
|
||||
})
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'MOCK_NOT_FOUND': {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
case 'NETWORK_ERROR': {
|
||||
const { name, message } = clientMessage.data
|
||||
const networkError = new Error(message)
|
||||
networkError.name = name
|
||||
|
||||
// Rejecting a "respondWith" promise emulates a network error.
|
||||
throw networkError
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
function sendToClient(client, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [channel.port2])
|
||||
})
|
||||
}
|
||||
|
||||
function sleep(timeMs) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, timeMs)
|
||||
})
|
||||
}
|
||||
|
||||
async function respondWithMock(response) {
|
||||
await sleep(response.delay)
|
||||
return new Response(response.body, response)
|
||||
}
|
||||
@@ -1,21 +1,14 @@
|
||||
import type { DeploymentRowFragment } from '@/generated/graphql';
|
||||
import { useGetDeploymentsSubSubscription } from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import Status, { StatusEnum } from '@/ui/Status';
|
||||
import type { DeploymentStatus } from '@/ui/StatusCircle';
|
||||
import { StatusCircle } from '@/ui/StatusCircle';
|
||||
import { getLastLiveDeployment } from '@/utils/helpers';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
|
||||
import {
|
||||
differenceInSeconds,
|
||||
formatDistanceToNowStrict,
|
||||
parseISO,
|
||||
} from 'date-fns';
|
||||
useGetDeploymentsSubSubscription,
|
||||
useLatestLiveDeploymentSubSubscription,
|
||||
useScheduledOrPendingDeploymentsSubSubscription,
|
||||
} from '@/generated/graphql';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type AppDeploymentsProps = {
|
||||
appId: string;
|
||||
@@ -66,146 +59,8 @@ function NextPrevPageLink(props: NextPrevPageLinkProps) {
|
||||
);
|
||||
}
|
||||
|
||||
type AppDeploymentDurationProps = {
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
};
|
||||
|
||||
export function AppDeploymentDuration({
|
||||
startedAt,
|
||||
endedAt,
|
||||
}: AppDeploymentDurationProps) {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
if (!endedAt) {
|
||||
interval = setInterval(() => {
|
||||
setCurrentTime(new Date());
|
||||
}, 1000);
|
||||
}
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [endedAt]);
|
||||
|
||||
const totalDurationInSeconds = differenceInSeconds(
|
||||
endedAt ? parseISO(endedAt) : currentTime,
|
||||
parseISO(startedAt),
|
||||
);
|
||||
|
||||
if (totalDurationInSeconds > 1200) {
|
||||
return <div>20+m</div>;
|
||||
}
|
||||
|
||||
const durationMins = Math.floor(totalDurationInSeconds / 60);
|
||||
const durationSecs = totalDurationInSeconds % 60;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
className="self-center font-display text-sm+ text-greyscaleDark"
|
||||
>
|
||||
{durationMins}m {durationSecs}s
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type AppDeploymentRowProps = {
|
||||
deployment: DeploymentRowFragment;
|
||||
isDeploymentLive: boolean;
|
||||
};
|
||||
|
||||
export function AppDeploymentRow({
|
||||
deployment,
|
||||
isDeploymentLive,
|
||||
}: AppDeploymentRowProps) {
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
|
||||
const { commitMessage } = deployment;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center px-2 py-4">
|
||||
<div className="mr-2 flex items-center justify-center">
|
||||
<Avatar
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4 w-full">
|
||||
<Link
|
||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
|
||||
>
|
||||
<div className="max-w-md truncate text-sm+ font-normal text-greyscaleDark">
|
||||
{commitMessage?.trim() || (
|
||||
<span className="pr-1 font-normal italic">
|
||||
No commit message
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm+ text-greyscaleGrey">
|
||||
{formatDistanceToNowStrict(
|
||||
parseISO(deployment.deploymentStartedAt),
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
{isDeploymentLive && (
|
||||
<div className="flex self-center align-middle">
|
||||
<Status status={StatusEnum.Live}>Live</Status>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-28 self-center text-right font-mono text-sm- font-medium">
|
||||
<a
|
||||
className="font-mono font-medium text-greyscaleDark"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={`https://github.com/${currentApplication.githubRepository?.fullName}/commit/${deployment.commitSHA}`}
|
||||
>
|
||||
{deployment.commitSHA.substring(0, 7)}
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx-4 w-28 text-right">
|
||||
<AppDeploymentDuration
|
||||
startedAt={deployment.deploymentStartedAt}
|
||||
endedAt={deployment.deploymentEndedAt}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-3 self-center">
|
||||
<StatusCircle
|
||||
status={deployment.deploymentStatus as DeploymentStatus}
|
||||
/>
|
||||
</div>
|
||||
<div className="self-center">
|
||||
<Link
|
||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
|
||||
passHref
|
||||
>
|
||||
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
const { appId } = props;
|
||||
const [idOfLiveDeployment, setIdOfLiveDeployment] = useState('');
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -216,36 +71,59 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
const limit = 10;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// @TODO: Should query for all deployments, then subscribe to new ones.
|
||||
|
||||
const { data, loading, error } = useGetDeploymentsSubSubscription({
|
||||
variables: {
|
||||
id: appId,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
const {
|
||||
data: deploymentPageData,
|
||||
loading: deploymentPageLoading,
|
||||
error,
|
||||
} = useGetDeploymentsSubSubscription({
|
||||
variables: { id: appId, limit, offset },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const { data: latestDeploymentData, loading: latestDeploymentLoading } =
|
||||
useGetDeploymentsSubSubscription({
|
||||
variables: { id: appId, limit: 1, offset: 0 },
|
||||
});
|
||||
|
||||
if (page === 1) {
|
||||
setIdOfLiveDeployment(getLastLiveDeployment(data.deployments));
|
||||
}
|
||||
}, [data, idOfLiveDeployment, loading, page]);
|
||||
const {
|
||||
data: latestLiveDeploymentData,
|
||||
loading: latestLiveDeploymentLoading,
|
||||
} = useLatestLiveDeploymentSubSubscription({ variables: { appId } });
|
||||
|
||||
const {
|
||||
data: scheduledOrPendingDeploymentsData,
|
||||
loading: scheduledOrPendingDeploymentsLoading,
|
||||
} = useScheduledOrPendingDeploymentsSubSubscription({ variables: { appId } });
|
||||
|
||||
const loading =
|
||||
deploymentPageLoading ||
|
||||
scheduledOrPendingDeploymentsLoading ||
|
||||
latestDeploymentLoading ||
|
||||
latestLiveDeploymentLoading;
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} className="mt-12" />;
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
className="mt-12"
|
||||
label="Loading deployments..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const nrOfDeployments = data.deployments.length;
|
||||
const { deployments } = deploymentPageData || {};
|
||||
const { deployments: scheduledOrPendingDeployments } =
|
||||
scheduledOrPendingDeploymentsData || {};
|
||||
|
||||
const latestDeployment = latestDeploymentData?.deployments[0];
|
||||
const latestLiveDeployment = latestLiveDeploymentData?.deployments[0];
|
||||
|
||||
const nrOfDeployments = deployments?.length || 0;
|
||||
const nextAllowed = !(nrOfDeployments < limit);
|
||||
const liveDeploymentId = latestLiveDeployment?.id || '';
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
@@ -253,15 +131,17 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
<p className="text-sm text-greyscaleGrey">No deployments yet.</p>
|
||||
) : (
|
||||
<div>
|
||||
<div className="mt-3 divide-y-1 border-t border-b">
|
||||
{data.deployments.map((deployment) => (
|
||||
<AppDeploymentRow
|
||||
deployment={deployment}
|
||||
<List className="mt-3 divide-y-1 border-t border-b">
|
||||
{deployments.map((deployment) => (
|
||||
<DeploymentListItem
|
||||
key={deployment.id}
|
||||
isDeploymentLive={idOfLiveDeployment === deployment.id}
|
||||
deployment={deployment}
|
||||
isLive={liveDeploymentId === deployment.id}
|
||||
showRedeploy={latestDeployment.id === deployment.id}
|
||||
disableRedeploy={scheduledOrPendingDeployments.length > 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</List>
|
||||
<div className="mt-8 flex w-full justify-center">
|
||||
<div className="flex items-center">
|
||||
<NextPrevPageLink
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useInsertFeatureFlagMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserEmail } from '@nhost/react';
|
||||
import { useUserEmail } from '@nhost/nextjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { RemoveApplicationModal } from './RemoveApplicationModal';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { inputErrorMessages } from '@/utils/getErrorMessage';
|
||||
import { slugifyString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useUpdateWorkspaceMutation } from '@/utils/__generated__/graphql';
|
||||
import router from 'next/router';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type ChangeWorkspaceNameProps = {
|
||||
close: VoidFunction;
|
||||
};
|
||||
|
||||
export default function ChangeWorkspaceName({
|
||||
close,
|
||||
}: ChangeWorkspaceNameProps) {
|
||||
const { currentWorkspace } = useCurrentWorkspaceAndApplication();
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState(
|
||||
currentWorkspace.name,
|
||||
);
|
||||
const [workspaceError, setWorkspaceError] = useState<string>('');
|
||||
|
||||
const [updateWorkspace, { loading: mutationLoading, error: mutationError }] =
|
||||
useUpdateWorkspaceMutation({
|
||||
refetchQueries: [],
|
||||
});
|
||||
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
inputErrorMessages(
|
||||
event.target.value,
|
||||
setNewWorkspaceName,
|
||||
setWorkspaceError,
|
||||
'Workspace',
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
const name = newWorkspaceName;
|
||||
const slug = slugifyString(name);
|
||||
|
||||
if (slug.length < 4 || slug.length > 32) {
|
||||
setWorkspaceError('Slug should be within 4 and 32 characters.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateWorkspace({
|
||||
variables: {
|
||||
id: currentWorkspace.id,
|
||||
workspace: {
|
||||
name,
|
||||
slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
close();
|
||||
triggerToast('Workspace name changed');
|
||||
} catch (error) {
|
||||
await discordAnnounce(
|
||||
`Error trying to remove workspace: ${currentWorkspace.id} - ${error.message}`,
|
||||
);
|
||||
}
|
||||
await router.push(slug);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h3" component="h2">
|
||||
Change Workspace Name
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mt-4 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
id="workspaceName"
|
||||
label="New Workspace Name"
|
||||
onChange={handleChange}
|
||||
value={newWorkspaceName}
|
||||
placeholder="New workspace name"
|
||||
fullWidth
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
helperText={`https://app.nhost.io/${slugifyString(
|
||||
newWorkspaceName || '',
|
||||
)}`}
|
||||
/>
|
||||
|
||||
{workspaceError && <Alert severity="error">{workspaceError}</Alert>}
|
||||
|
||||
{mutationError && (
|
||||
<Alert severity="error">{mutationError.toString()}</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={mutationLoading || !!workspaceError}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,9 +32,12 @@ export default function ConnectGithubModal({ close }: ConnectGithubModalProps) {
|
||||
useState<ConnectGithubModalState>('CONNECTING');
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
|
||||
|
||||
const { data, loading, error } = useGetGithubRepositoriesQuery({
|
||||
pollInterval: 2000,
|
||||
});
|
||||
const { data, loading, error, startPolling } =
|
||||
useGetGithubRepositoriesQuery();
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(2000);
|
||||
}, [startPolling]);
|
||||
|
||||
const handleSelectAnotherRepository = () => {
|
||||
setSelectedRepoId(null);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { ConnectionDetail } from '@/components/applications/ConnectionDetail';
|
||||
import { LoadingScreen } from '@/components/common/LoadingScreen';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import generateAppServiceUrl, {
|
||||
defaultLocalBackendSlugs,
|
||||
defaultRemoteBackendSlugs,
|
||||
} from '@/utils/common/generateAppServiceUrl';
|
||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||
import { generateAppServiceUrl } from '@/utils/helpers';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface HasuraDataProps {
|
||||
@@ -15,6 +19,7 @@ interface HasuraDataProps {
|
||||
|
||||
export function HasuraData({ close }: HasuraDataProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
if (
|
||||
!currentApplication?.subdomain ||
|
||||
@@ -24,13 +29,15 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
}
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? LOCAL_HASURA_URL
|
||||
: `${generateAppServiceUrl(
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||
? `${LOCAL_HASURA_URL}/console`
|
||||
: generateAppServiceUrl(
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region.awsName,
|
||||
'hasura',
|
||||
)}/console`;
|
||||
defaultLocalBackendSlugs,
|
||||
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md px-6 py-4 text-left">
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export type FunctionLog = {
|
||||
name: string;
|
||||
language: string;
|
||||
logs: { date: string; message: string; createdAt: string }[];
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import { formatDistance } from 'date-fns';
|
||||
|
||||
export interface FunctionLogDataEntryProps {
|
||||
time: string;
|
||||
nav: string;
|
||||
}
|
||||
|
||||
export function FunctionLogDataEntry({ time, nav }: FunctionLogDataEntryProps) {
|
||||
return (
|
||||
<a href={`#${nav}`}>
|
||||
<div className="flex cursor-pointer flex-row place-content-between border-t py-3">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
variant="body"
|
||||
className="flex font-medium"
|
||||
size="tiny"
|
||||
>
|
||||
{formatDistance(new Date(time), new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</Text>
|
||||
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center text-greyscaleDark" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunctionLogDataEntry;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Text } from '@/ui/Text';
|
||||
import { FunctionLogDataEntry } from './FunctionLogDataEntry';
|
||||
|
||||
export interface FunctionLogHistoryProps {
|
||||
logs?: Log[];
|
||||
}
|
||||
|
||||
type Log = {
|
||||
createdAt: string;
|
||||
date: any;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function FunctionLogHistory({ logs }: FunctionLogHistoryProps) {
|
||||
return (
|
||||
<div className=" mx-auto max-w-6xl pt-10">
|
||||
<div className="flex flex-row place-content-between">
|
||||
<div className="flex">
|
||||
<Text size="large" className="font-medium" color="greyscaleDark">
|
||||
Log History
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col">
|
||||
<div className="flex flex-row">
|
||||
<Text className="font-semibold" size="normal" color="greyscaleDark">
|
||||
Time
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{logs ? (
|
||||
<div>
|
||||
{logs.slice(0, 4).map((log: Log) => (
|
||||
<FunctionLogDataEntry
|
||||
time={log.createdAt}
|
||||
nav={`#-${log.date}`}
|
||||
key={`${log.date}-${log.message.slice(66)}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="pt-1 pl-0.5 font-mono text-xs text-greyscaleDark">
|
||||
No log history.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunctionLogHistory;
|
||||
@@ -1,86 +0,0 @@
|
||||
import { normalizeToIndividualFunctionsWithLogs } from '@/components/applications/functions/normalizeToIndividualFunctionsWithLogs';
|
||||
import terminalTheme from '@/data/terminalTheme';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useGetFunctionLogQuery } from '@/utils/__generated__/graphql';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
|
||||
import { FunctionLogHistory } from './FunctionLogHistory';
|
||||
|
||||
SyntaxHighlighter.registerLanguage('json', json);
|
||||
|
||||
export function FunctionsLogsTerminalPage({ functionName }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [normalizedFunctionData, setNormalizedFunctionData] = useState(null);
|
||||
|
||||
const { data } = useGetFunctionLogQuery({
|
||||
variables: {
|
||||
subdomain: currentApplication.subdomain,
|
||||
functionPaths: [functionName?.split('/').slice(1, 3).join('/')],
|
||||
},
|
||||
pollInterval: 3000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || data.getFunctionLogs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setNormalizedFunctionData(
|
||||
normalizeToIndividualFunctionsWithLogs(data.getFunctionLogs)[0],
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
if (
|
||||
!data ||
|
||||
data.getFunctionLogs.length === 0 ||
|
||||
!normalizedFunctionData ||
|
||||
normalizedFunctionData.logs.length === 0
|
||||
) {
|
||||
return (
|
||||
<div className="w-full rounded-lg text-white">
|
||||
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
|
||||
<div className="font-mono text-xs text-grey">
|
||||
There are no stored logs yet. Try calling your function for logs to
|
||||
appear.
|
||||
</div>
|
||||
</div>
|
||||
<FunctionLogHistory />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="w-full rounded-lg text-white">
|
||||
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
|
||||
{normalizedFunctionData.logs.map((log) => (
|
||||
<div
|
||||
key={`${log.date}-${log.message.slice(66)}`}
|
||||
className=" flex text-sm"
|
||||
>
|
||||
<div id={`#-${log.date}`}>
|
||||
<pre className="inline">
|
||||
<span className="mr-4 text-greyscaleGrey">{log.date}</span>{' '}
|
||||
<span className="">
|
||||
{' '}
|
||||
<SyntaxHighlighter
|
||||
style={terminalTheme}
|
||||
customStyle={{
|
||||
display: 'inline',
|
||||
}}
|
||||
className="inline-flex"
|
||||
language="json"
|
||||
>
|
||||
{log.message}
|
||||
</SyntaxHighlighter>
|
||||
</span>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<FunctionLogHistory logs={normalizedFunctionData.logs} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunctionsLogsTerminalPage;
|
||||
@@ -1,5 +0,0 @@
|
||||
export type FunctionResponseLog = {
|
||||
functionPath: string;
|
||||
createdAt: string;
|
||||
message: string;
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Button } from '@/ui/Button';
|
||||
import Loading from '@/ui/Loading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function FunctionsNotDeployed() {
|
||||
return (
|
||||
<div className="mx-auto mt-12 max-w-2xl text-center">
|
||||
<div className="mx-auto flex w-centImage flex-col text-center">
|
||||
<Image
|
||||
src="/terminal-text.svg"
|
||||
alt="Terminal with a green dot"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
</div>
|
||||
<Text className="mt-4 font-medium" size="large" color="dark">
|
||||
Functions Logs
|
||||
</Text>
|
||||
<Text size="normal" color="greyscaleDark" className="mt-1 transform">
|
||||
Once you deploy a function, you can view the logs here.
|
||||
</Text>
|
||||
<div className="mt-1.5 flex text-center">
|
||||
<Button
|
||||
Component="a"
|
||||
transparent
|
||||
color="blue"
|
||||
className="mx-auto cursor-pointer font-medium"
|
||||
href="https://docs.nhost.io/platform/serverless-functions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read more
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-24 flex flex-col text-center">
|
||||
<Loading />
|
||||
<Text size="normal" color="greyscaleDark" className="mt-1 transform">
|
||||
Awaiting new requests…
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunctionsNotDeployed;
|
||||
@@ -1,79 +0,0 @@
|
||||
export type FinalFunction = {
|
||||
folder: string;
|
||||
funcs: Func[];
|
||||
nestedLevel: number;
|
||||
parentFolder?: string;
|
||||
};
|
||||
|
||||
export type Func = {
|
||||
name: string;
|
||||
id: string;
|
||||
lang: string;
|
||||
functionName: string;
|
||||
route?: string;
|
||||
path?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
createdWithCommitSha?: string;
|
||||
formattedCreatedAt?: string;
|
||||
formattedUpdatedAt?: string;
|
||||
};
|
||||
|
||||
export const normalizeFunctionMetadata = (functions): FinalFunction[] => {
|
||||
const finalFunctions: FinalFunction[] = [
|
||||
{ folder: 'functions', funcs: [], nestedLevel: 0 },
|
||||
];
|
||||
const topLevelFunctionsFolder = finalFunctions[0].funcs;
|
||||
functions.forEach((func) => {
|
||||
const nestedLevel = func.path?.split('/').length;
|
||||
const newFuncToAdd = {
|
||||
...func,
|
||||
name: func.path?.split('/')[nestedLevel - 1],
|
||||
lang: func.path?.split('.')[1],
|
||||
// formattedCreatedAt: `${format(
|
||||
// parseISO(func.createdAt),
|
||||
// 'yyyy-MM-dd HH:mm:ss',
|
||||
// )}`,
|
||||
// formattedUpdatedAt: `${formatDistanceToNowStrict(
|
||||
// parseISO(func.updatedAt),
|
||||
// {
|
||||
// addSuffix: true,
|
||||
// },
|
||||
// )}`,
|
||||
};
|
||||
|
||||
if (nestedLevel === 2) {
|
||||
topLevelFunctionsFolder.push(newFuncToAdd);
|
||||
} else if (nestedLevel > 2) {
|
||||
const nameOfTheFolder = func.path?.split('/')[nestedLevel - 2];
|
||||
const nameOfParentFolder = func.path?.split('/')[nestedLevel - 3];
|
||||
const checkForFolderExistence = finalFunctions.find(
|
||||
(functionFolder) => functionFolder.folder === nameOfTheFolder,
|
||||
);
|
||||
|
||||
if (!checkForFolderExistence) {
|
||||
finalFunctions.push({
|
||||
folder: nameOfTheFolder,
|
||||
funcs: [newFuncToAdd],
|
||||
nestedLevel: nestedLevel - 2,
|
||||
parentFolder: nameOfParentFolder,
|
||||
});
|
||||
} else {
|
||||
checkForFolderExistence.funcs.push(newFuncToAdd);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Sort folders by putting the subfolder next to their parent folder, even though they share the same place in the array
|
||||
// except for the nestedLevel prop. A future change to this would be to make folders have subfolders, which is easier
|
||||
// understand, but would require a change in the UI.
|
||||
// @TODO: Change to have elements have subfolders inside the object?
|
||||
finalFunctions.sort((a, b) => {
|
||||
if (a.folder === b.parentFolder) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
|
||||
return finalFunctions;
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import type { FunctionLog } from './FunctionLog';
|
||||
import type { FunctionResponseLog } from './FunctionResponseLog';
|
||||
|
||||
export const normalizeToIndividualFunctionsWithLogs = (
|
||||
functionLogs: FunctionResponseLog[],
|
||||
) => {
|
||||
const arrayOfFunctions: FunctionLog[] = [];
|
||||
const sortedFunctions = [...functionLogs].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
|
||||
sortedFunctions.forEach((functionLog) => {
|
||||
const funcName = functionLog.functionPath;
|
||||
const logMessage = {
|
||||
createdAt: functionLog.createdAt,
|
||||
date: `${format(parseISO(functionLog.createdAt), 'yyyy-MM-dd HH:mm:ss')}`,
|
||||
message: functionLog.message,
|
||||
};
|
||||
const newFunc = {
|
||||
name: funcName,
|
||||
language: functionLog.functionPath.split('.')[1],
|
||||
logs: [logMessage],
|
||||
};
|
||||
// If the function is already in the array of functions to log, just add the new log message to the existing object...
|
||||
if (arrayOfFunctions.some((obj) => obj.name === funcName)) {
|
||||
const index = arrayOfFunctions.findIndex((obj) => obj.name === funcName);
|
||||
const currentFunction = arrayOfFunctions[index];
|
||||
currentFunction.logs.push(logMessage);
|
||||
} else {
|
||||
// If the function is not in the array of functions, add it with the log message to it.
|
||||
arrayOfFunctions.push(newFunc);
|
||||
}
|
||||
});
|
||||
|
||||
return arrayOfFunctions;
|
||||
};
|
||||
|
||||
export default normalizeToIndividualFunctionsWithLogs;
|
||||
@@ -34,13 +34,13 @@ export function Repo({ repo, setSelectedRepoId }: RepoProps) {
|
||||
const [updateApp, { loading, error }] = useUpdateAppMutation({
|
||||
refetchQueries: [
|
||||
refetchGetAppByWorkspaceAndNameQuery({
|
||||
workspace: currentWorkspace.slug,
|
||||
slug: currentApplication.slug,
|
||||
workspace: currentWorkspace?.slug,
|
||||
slug: currentApplication?.slug,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { githubRepository } = currentApplication;
|
||||
const { githubRepository } = currentApplication || {};
|
||||
|
||||
const isThisRepositoryAlreadyConnected =
|
||||
githubRepository?.fullName && githubRepository.fullName === repo.fullName;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLCl
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import Select from '@/ui/v2/Select';
|
||||
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
||||
import type { RemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import { useRemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import { DEFAULT_ROLES } from './utils';
|
||||
|
||||
@@ -57,7 +57,7 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user: RemoteAppGetUsersQuery['users'][number] = users.find(
|
||||
const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
|
||||
({ id }) => id === userId,
|
||||
);
|
||||
|
||||
|
||||
@@ -37,20 +37,29 @@ function ControlledAutocomplete(
|
||||
}: ControlledAutocompleteProps<AutocompleteOption>,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
const { setValue } = useFormContext();
|
||||
const form = useFormContext();
|
||||
const { field } = useController({
|
||||
...controllerProps,
|
||||
...(controllerProps || {}),
|
||||
name: controllerProps?.name || name || '',
|
||||
control: controllerProps?.control || control,
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
throw new Error('ControlledAutocomplete must be used in a FormContext.');
|
||||
}
|
||||
|
||||
const { setValue } = form || {};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
inputValue={typeof field.value === 'string' ? field.value : undefined}
|
||||
{...props}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, options, reason, details) => {
|
||||
setValue(controllerProps?.name || name, options);
|
||||
setValue?.(controllerProps?.name || name, options, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event, options, reason, details);
|
||||
|
||||
@@ -42,13 +42,16 @@ function ControlledSwitch(
|
||||
{...props}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(e) => {
|
||||
setValue(controllerProps?.name || name, e.target.checked, {
|
||||
onChange={(event) => {
|
||||
setValue(controllerProps?.name || name, event.target.checked, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event);
|
||||
}
|
||||
}}
|
||||
checked={field.value || false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import DataGridBody from '@/components/common/DataGridBody';
|
||||
import DataGridFrame from '@/components/common/DataGridFrame';
|
||||
import type { DataGridHeaderProps } from '@/components/common/DataGridHeader';
|
||||
import DataGridHeader from '@/components/common/DataGridHeader';
|
||||
import DataBrowserEmptyState from '@/components/data-browser/DataBrowserEmptyState';
|
||||
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
|
||||
import { DataGridProvider } from '@/context/DataGridContext';
|
||||
import type { UseDataGridOptions } from '@/hooks/useDataGrid';
|
||||
import useDataGrid from '@/hooks/useDataGrid';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef, useEffect, useRef } from 'react';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DataGridProps } from '@/components/common/DataGrid';
|
||||
import DataGridCell from '@/components/common/DataGridCell';
|
||||
import useDataGridConfig from '@/hooks/useDataGridConfig';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
|
||||
@@ -35,15 +35,15 @@ function InsertPlaceholderTableRow({
|
||||
}: InsertPlaceholderTableRowProps) {
|
||||
return (
|
||||
<div
|
||||
className="h-12 border-r-1 border-b-1 border-gray-200 bg-white"
|
||||
className="h-12 bg-white border-gray-200 border-r-1 border-b-1"
|
||||
{...props}
|
||||
>
|
||||
<Button
|
||||
onClick={onInsertRow}
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="h-full w-full justify-start rounded-none px-2 py-3 text-xs font-normal hover:shadow-none focus:shadow-none focus:outline-none"
|
||||
startIcon={<PlusIcon className="h-4 w-4 text-greyscaleGrey" />}
|
||||
className="justify-start w-full h-full px-2 py-3 text-xs font-normal rounded-none hover:shadow-none focus:shadow-none focus:outline-none"
|
||||
startIcon={<PlusIcon className="w-4 h-4 text-greyscaleGrey" />}
|
||||
>
|
||||
Insert New Row
|
||||
</Button>
|
||||
@@ -181,7 +181,7 @@ export default function DataGridBody<T extends object>({
|
||||
return (
|
||||
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
||||
{rows.length === 0 && !loading && (
|
||||
<div className="flex flex-nowrap pr-5">
|
||||
<div className="flex pr-5 flex-nowrap">
|
||||
{onInsertRow ? (
|
||||
<InsertPlaceholderTableRow
|
||||
style={{
|
||||
@@ -279,7 +279,7 @@ export default function DataGridBody<T extends object>({
|
||||
})}
|
||||
|
||||
{allowInsertColumn && (
|
||||
<div className="h-12 w-25 border-r-1 border-b-1 border-gray-200 bg-white" />
|
||||
<div className="h-12 bg-white border-gray-200 w-25 border-r-1 border-b-1" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ColumnType,
|
||||
DataBrowserGridCell,
|
||||
DataBrowserGridCellProps,
|
||||
} from '@/types/data-browser';
|
||||
} from '@/types/dataBrowser';
|
||||
import Tooltip, { useTooltip } from '@/ui/v2/Tooltip';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import type {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataGridProps } from '@/components/common/DataGrid';
|
||||
import useDataGridConfig from '@/hooks/useDataGridConfig';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
|
||||
@@ -6,19 +6,25 @@ 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_ENVIRONMENT_VARIABLE'
|
||||
| 'EDIT_USER'
|
||||
| 'EDIT_USER_PASSWORD'
|
||||
| 'EDIT_JWT_SECRET';
|
||||
|
||||
export interface DialogConfig<TPayload = unknown> {
|
||||
/**
|
||||
@@ -62,6 +68,16 @@ export interface DialogContextProps {
|
||||
* Call this function to close the active drawer.
|
||||
*/
|
||||
closeDrawer: VoidFunction;
|
||||
/**
|
||||
* Call this function to check if the form is dirty and close the active dialog
|
||||
* if the form is pristine.
|
||||
*/
|
||||
closeDialogWithDirtyGuard: VoidFunction;
|
||||
/**
|
||||
* Call this function to check if the form is dirty and close the active drawer
|
||||
* if the form is pristine.
|
||||
*/
|
||||
closeDrawerWithDirtyGuard: VoidFunction;
|
||||
/**
|
||||
* Call this function to close the active alert dialog.
|
||||
*/
|
||||
@@ -73,6 +89,10 @@ export interface DialogContextProps {
|
||||
isDirty: boolean,
|
||||
location?: 'drawer' | 'dialog',
|
||||
) => void;
|
||||
/**
|
||||
* Call this function to open a dirty confirmation dialog.
|
||||
*/
|
||||
openDirtyConfirmation: (config?: Partial<DialogConfig<string>>) => void;
|
||||
}
|
||||
|
||||
export default createContext<DialogContextProps>({
|
||||
@@ -81,6 +101,9 @@ export default createContext<DialogContextProps>({
|
||||
openAlertDialog: () => {},
|
||||
closeDialog: () => {},
|
||||
closeDrawer: () => {},
|
||||
closeDialogWithDirtyGuard: () => {},
|
||||
closeDrawerWithDirtyGuard: () => {},
|
||||
closeAlertDialog: () => {},
|
||||
onDirtyStateChange: () => {},
|
||||
openDirtyConfirmation: () => {},
|
||||
});
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import CreateForeignKeyForm from '@/components/data-browser/CreateForeignKeyForm';
|
||||
import EditForeignKeyForm from '@/components/data-browser/EditForeignKeyForm';
|
||||
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 { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DialogConfig, DialogType } from './DialogContext';
|
||||
import DialogContext from './DialogContext';
|
||||
@@ -49,31 +62,38 @@ function LoadingComponent({
|
||||
}
|
||||
|
||||
const CreateRecordForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateRecordForm'),
|
||||
() => import('@/components/dataBrowser/CreateRecordForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateColumnForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateColumnForm'),
|
||||
() => import('@/components/dataBrowser/CreateColumnForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditColumnForm = dynamic(
|
||||
() => import('@/components/data-browser/EditColumnForm'),
|
||||
() => import('@/components/dataBrowser/EditColumnForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateTableForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateTableForm'),
|
||||
() => import('@/components/dataBrowser/CreateTableForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditTableForm = dynamic(
|
||||
() => import('@/components/data-browser/EditTableForm'),
|
||||
() => 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,
|
||||
@@ -161,42 +181,52 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
alertDialogDispatch({ type: 'CLEAR_ALERT_CONTENT' });
|
||||
}
|
||||
|
||||
function openDirtyConfirmation(config?: Partial<DialogConfig<string>>) {
|
||||
const { props, ...restConfig } = config || {};
|
||||
const openDirtyConfirmation = useCallback(
|
||||
(config?: Partial<DialogConfig<string>>) => {
|
||||
const { props, ...restConfig } = config || {};
|
||||
|
||||
openAlertDialog({
|
||||
...config,
|
||||
title: 'Unsaved changes',
|
||||
payload:
|
||||
'You have unsaved local changes. Are you sure you want to discard them?',
|
||||
props: {
|
||||
...props,
|
||||
primaryButtonText: 'Discard',
|
||||
primaryButtonColor: 'error',
|
||||
},
|
||||
...restConfig,
|
||||
});
|
||||
}
|
||||
|
||||
function closeDrawerWithDirtyGuard(event?: BaseSyntheticEvent) {
|
||||
if (isDrawerDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } });
|
||||
return;
|
||||
}
|
||||
openAlertDialog({
|
||||
...config,
|
||||
title: 'Unsaved changes',
|
||||
payload:
|
||||
'You have unsaved local changes. Are you sure you want to discard them?',
|
||||
props: {
|
||||
...props,
|
||||
primaryButtonText: 'Discard',
|
||||
primaryButtonColor: 'error',
|
||||
},
|
||||
...restConfig,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
closeDrawer();
|
||||
}
|
||||
const closeDrawerWithDirtyGuard = useCallback(
|
||||
(event?: BaseSyntheticEvent) => {
|
||||
if (isDrawerDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } });
|
||||
return;
|
||||
}
|
||||
|
||||
function closeDialogWithDirtyGuard(event?: BaseSyntheticEvent) {
|
||||
if (isDialogDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } });
|
||||
return;
|
||||
}
|
||||
closeDrawer();
|
||||
},
|
||||
[closeDrawer, openDirtyConfirmation],
|
||||
);
|
||||
|
||||
closeDialog();
|
||||
}
|
||||
const closeDialogWithDirtyGuard = useCallback(
|
||||
(event?: BaseSyntheticEvent) => {
|
||||
if (isDialogDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } });
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
},
|
||||
[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
|
||||
@@ -223,10 +253,22 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
openAlertDialog,
|
||||
closeDialog,
|
||||
closeDrawer,
|
||||
closeDialogWithDirtyGuard,
|
||||
closeDrawerWithDirtyGuard,
|
||||
closeAlertDialog,
|
||||
onDirtyStateChange,
|
||||
openDirtyConfirmation,
|
||||
}),
|
||||
[closeDialog, closeDrawer, onDirtyStateChange, openDialog, openDrawer],
|
||||
[
|
||||
closeDialog,
|
||||
closeDialogWithDirtyGuard,
|
||||
closeDrawer,
|
||||
closeDrawerWithDirtyGuard,
|
||||
onDirtyStateChange,
|
||||
openDialog,
|
||||
openDirtyConfirmation,
|
||||
openDrawer,
|
||||
],
|
||||
);
|
||||
|
||||
const sharedDialogProps = {
|
||||
@@ -248,6 +290,32 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
onCancel: closeDrawerWithDirtyGuard,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function handleCloseDrawerAndDialog() {
|
||||
if (isDrawerDirty.current || isDialogDirty.current) {
|
||||
openDirtyConfirmation({
|
||||
props: {
|
||||
onPrimaryAction: () => {
|
||||
closeDialog();
|
||||
closeDrawer();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
throw new Error('Unsaved changes');
|
||||
}
|
||||
|
||||
closeDrawer();
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
router?.events?.on?.('routeChangeStart', handleCloseDrawerAndDialog);
|
||||
|
||||
return () => {
|
||||
router?.events?.off?.('routeChangeStart', handleCloseDrawerAndDialog);
|
||||
};
|
||||
}, [closeDialog, closeDrawer, openDirtyConfirmation, router.events]);
|
||||
|
||||
return (
|
||||
<DialogContext.Provider value={contextValue}>
|
||||
<AlertDialog
|
||||
@@ -299,6 +367,10 @@ 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} />
|
||||
)}
|
||||
@@ -315,6 +387,10 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<EditRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_USER' && (
|
||||
<CreateUserForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_PERMISSION_VARIABLE' && (
|
||||
<CreatePermissionVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
@@ -330,17 +406,34 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
{activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && (
|
||||
<EditEnvironmentVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_USER_PASSWORD' && (
|
||||
<EditUserPasswordForm
|
||||
{...sharedDialogProps}
|
||||
user={sharedDialogProps?.user}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_JWT_SECRET' && (
|
||||
<EditJwtSecretForm {...sharedDialogProps} />
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</BaseDialog>
|
||||
|
||||
<Drawer
|
||||
anchor="right"
|
||||
{...drawerProps}
|
||||
title={drawerTitle}
|
||||
open={drawerOpen}
|
||||
onClose={closeDrawerWithDirtyGuard}
|
||||
SlideProps={{ onExited: clearDrawerContent, unmountOnExit: false }}
|
||||
anchor="right"
|
||||
PaperProps={{ className: 'max-w-2.5xl w-full' }}
|
||||
PaperProps={{
|
||||
...drawerProps?.PaperProps,
|
||||
className: twMerge(
|
||||
'max-w-2.5xl w-full',
|
||||
drawerProps?.PaperProps?.className,
|
||||
),
|
||||
}}
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
{activeDrawerType === 'CREATE_RECORD' && (
|
||||
@@ -375,6 +468,19 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
schema={drawerPayload?.schema}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_PERMISSIONS' && (
|
||||
<EditPermissionsForm
|
||||
{...sharedDrawerProps}
|
||||
disabled={drawerPayload?.disabled}
|
||||
schema={drawerPayload?.schema}
|
||||
table={drawerPayload?.table}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_USER' && (
|
||||
<EditUserForm {...sharedDrawerProps} {...drawerPayload} />
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</Drawer>
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
export default function HighlightedText({
|
||||
children,
|
||||
}: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<InlineCode className="text-greyscaleDark bg-primary-light font-display text-sm">
|
||||
{children}
|
||||
</InlineCode>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/components/common/HighlightedText/index.ts
Normal file
1
dashboard/src/components/common/HighlightedText/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HighlightedText';
|
||||
@@ -8,7 +8,7 @@ function InlineCode({ className, children, ...props }: InlineCodeProps) {
|
||||
return (
|
||||
<code
|
||||
className={twMerge(
|
||||
'inline-grid h-full max-h-[18px] max-w-xs items-center truncate rounded-sm bg-gray-100 px-1 font-mono text-[11px] text-gray-600',
|
||||
'inline-grid max-w-xs items-center truncate rounded-sm bg-gray-100 px-1 font-mono text-[11px] text-greyscaleMedium',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
138
dashboard/src/components/common/Pagination/Pagination.tsx
Normal file
138
dashboard/src/components/common/Pagination/Pagination.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { ButtonProps } from '@/ui/v2/Button';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import ChevronLeftIcon from '@/ui/v2/icons/ChevronLeftIcon';
|
||||
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export type PaginationProps = DetailedHTMLProps<
|
||||
HTMLProps<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
/**
|
||||
* Total number of pages.
|
||||
*/
|
||||
totalNrOfPages: number;
|
||||
/**
|
||||
* Number of total elements per page.
|
||||
*/
|
||||
elementsPerPage?: number;
|
||||
/**
|
||||
* Total number of elements.
|
||||
*/
|
||||
totalNrOfElements: number;
|
||||
/**
|
||||
* Current page number.
|
||||
*/
|
||||
currentPageNumber: number;
|
||||
/**
|
||||
* Function to be called when navigating to the previous page.
|
||||
*/
|
||||
onPrevPageClick: VoidFunction;
|
||||
/**
|
||||
* Function to be called when navigating to the next page.
|
||||
*/
|
||||
onNextPageClick: VoidFunction;
|
||||
/**
|
||||
* Function to be called when a new page number is submitted.
|
||||
*/
|
||||
onPageChange: (page: number) => void;
|
||||
/**
|
||||
* Props for component slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props to be passed to the next button component.
|
||||
*/
|
||||
nextButton?: Partial<ButtonProps>;
|
||||
/**
|
||||
* Props to be passed to the previous button component.
|
||||
*/
|
||||
prevButton?: Partial<ButtonProps>;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Pagination({
|
||||
className,
|
||||
totalNrOfPages,
|
||||
currentPageNumber,
|
||||
onPrevPageClick,
|
||||
onNextPageClick,
|
||||
slotProps,
|
||||
elementsPerPage,
|
||||
onPageChange,
|
||||
totalNrOfElements,
|
||||
...props
|
||||
}: PaginationProps) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge('grid grid-flow-col items-center gap-2', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="grid justify-start grid-flow-col gap-2">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="block text-xs"
|
||||
disabled={currentPageNumber === 1}
|
||||
aria-label="Previous page"
|
||||
onClick={onPrevPageClick}
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<div className="grid items-center grid-cols-3 gap-1 text-center grid-col !text-greyscaleGreyDark">
|
||||
<Text className="text-xs align-middle ">Page</Text>
|
||||
<Input
|
||||
value={currentPageNumber}
|
||||
onChange={(e) => {
|
||||
const page = parseInt(e.target.value, 10);
|
||||
if (page > 0 && page <= totalNrOfPages) {
|
||||
onPageChange(page);
|
||||
}
|
||||
}}
|
||||
disabled={totalNrOfPages === 1}
|
||||
color="secondary"
|
||||
slotProps={{
|
||||
inputRoot: {
|
||||
className: 'w-4 h-2.5 text-center !text-[11.5px]',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text className="self-center text-xs align-middle text-greyscaleGreyDark">
|
||||
of {totalNrOfPages}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="text-xs"
|
||||
aria-label="Next page"
|
||||
disabled={currentPageNumber === totalNrOfPages}
|
||||
onClick={onNextPageClick}
|
||||
{...slotProps?.nextButton}
|
||||
>
|
||||
Next
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-end text-center gap-x-1">
|
||||
<Text className="text-xs text-greyscaleGreyDark">
|
||||
{currentPageNumber === 1 && currentPageNumber}
|
||||
{currentPageNumber === 2 && elementsPerPage + currentPageNumber - 1}
|
||||
{currentPageNumber > 2 &&
|
||||
(currentPageNumber - 1) * elementsPerPage + 1}{' '}
|
||||
-{' '}
|
||||
{totalNrOfElements < currentPageNumber * elementsPerPage
|
||||
? totalNrOfElements
|
||||
: currentPageNumber * elementsPerPage}{' '}
|
||||
of {totalNrOfElements} users
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
dashboard/src/components/common/Pagination/index.ts
Normal file
3
dashboard/src/components/common/Pagination/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './Pagination';
|
||||
export { default } from './Pagination';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import type { PropsWithoutRef } from 'react';
|
||||
|
||||
import type { ReadOnlyToggleProps } from './ReadOnlyToggle';
|
||||
import ReadOnlyToggle from './ReadOnlyToggle';
|
||||
|
||||
export default {
|
||||
title: 'Common Components / ReadOnlyToggle',
|
||||
component: ReadOnlyToggle,
|
||||
argTypes: {
|
||||
checked: {
|
||||
options: [null, true, false],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ReadOnlyToggle>;
|
||||
|
||||
const Template: ComponentStory<typeof ReadOnlyToggle> = function Template(
|
||||
args: PropsWithoutRef<ReadOnlyToggleProps>,
|
||||
) {
|
||||
return <ReadOnlyToggle {...args} />;
|
||||
};
|
||||
|
||||
export const Null = Template.bind({});
|
||||
Null.args = {
|
||||
checked: null,
|
||||
};
|
||||
|
||||
export const True = Template.bind({});
|
||||
True.args = {
|
||||
checked: true,
|
||||
};
|
||||
|
||||
export const False = Template.bind({});
|
||||
False.args = {
|
||||
checked: false,
|
||||
};
|
||||
|
||||
export const CustomClasses = Template.bind({});
|
||||
CustomClasses.args = {
|
||||
checked: true,
|
||||
className: '!bg-red',
|
||||
slotProps: {
|
||||
label: {
|
||||
className: '!text-sm !text-white',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,39 +1,79 @@
|
||||
import type { ForwardedRef } from 'react';
|
||||
import type { TextProps } from '@/ui/v2/Text';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const ReadOnlyToggle = forwardRef(
|
||||
(
|
||||
{ checked }: { checked: boolean | null },
|
||||
ref: ForwardedRef<HTMLSpanElement>,
|
||||
) => (
|
||||
export interface ReadOnlyToggleProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement> {
|
||||
/**
|
||||
* Determines whether the toggle is checked or not.
|
||||
*/
|
||||
checked?: boolean | null;
|
||||
/**
|
||||
* Props passed to specific component slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props passed to the root `<span />` element.
|
||||
*/
|
||||
root?: DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement>;
|
||||
/**
|
||||
* Props passed to the label.
|
||||
*/
|
||||
label?: TextProps;
|
||||
};
|
||||
}
|
||||
|
||||
function ReadOnlyToggle(
|
||||
{ checked, className, slotProps = {}, ...props }: ReadOnlyToggleProps,
|
||||
ref: ForwardedRef<HTMLSpanElement>,
|
||||
) {
|
||||
return (
|
||||
<span
|
||||
className="inline-grid h-full w-full grid-flow-col items-center justify-start gap-1.5"
|
||||
{...props}
|
||||
{...(slotProps?.root || {})}
|
||||
className={twMerge(
|
||||
'inline-grid h-full w-full grid-flow-col items-center justify-start gap-1.5',
|
||||
slotProps?.root?.className,
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
'box-border inline-grid h-3 w-5 items-center rounded-full px-0.5',
|
||||
checked === true && 'justify-end bg-greyscaleDark',
|
||||
checked === true &&
|
||||
'border-1 border-transparent justify-end bg-greyscaleDark',
|
||||
checked === false && 'border-1 border-greyscaleDark',
|
||||
checked === null && 'border-1 border-greyscaleDark',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
'inline rounded-full',
|
||||
'inline-block rounded-full',
|
||||
checked === true && 'h-2 w-2 bg-white',
|
||||
checked === false && 'h-2 w-2 bg-greyscaleDark',
|
||||
checked === null && 'h-px w-2 justify-self-center bg-greyscaleDark',
|
||||
checked === null &&
|
||||
'h-px my-px w-2 justify-self-center bg-greyscaleDark',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="truncate text-xs font-normal">{String(checked)}</span>
|
||||
<Text
|
||||
{...(slotProps?.label || {})}
|
||||
component="span"
|
||||
className={twMerge(
|
||||
'truncate !text-xs font-normal',
|
||||
slotProps?.label?.className,
|
||||
)}
|
||||
>
|
||||
{String(checked)}
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
ReadOnlyToggle.displayName = 'NhostReadOnlyToggle';
|
||||
|
||||
export default ReadOnlyToggle;
|
||||
export default forwardRef(ReadOnlyToggle);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { ChangePasswordModal } from '@/components/applications/ChangePasswordModal';
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import { useUserDataContext } from '@/context/workspace1-context';
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown, useDropdown } from '@/ui/v2/Dropdown';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { emptyWorkspace } from '@/utils/helpers';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -22,9 +20,8 @@ function AccountMenuContent({
|
||||
}: AccountMenuContentProps) {
|
||||
const user = useUserData();
|
||||
const router = useRouter();
|
||||
const client = useApolloClient();
|
||||
const [clicked, setClicked] = useState(false);
|
||||
const { setWorkspaceContext } = useWorkspaceContext();
|
||||
const { setUserContext } = useUserDataContext();
|
||||
const { handleClose } = useDropdown();
|
||||
|
||||
return (
|
||||
@@ -34,10 +31,9 @@ function AccountMenuContent({
|
||||
color="secondary"
|
||||
className="absolute top-6 right-4 grid grid-flow-col items-center gap-1 self-start font-medium"
|
||||
onClick={async () => {
|
||||
setWorkspaceContext(emptyWorkspace());
|
||||
setUserContext({ workspaces: [] });
|
||||
nhost.auth.signOut();
|
||||
router.push('/signin');
|
||||
await nhost.auth.signOut();
|
||||
await client.resetStore();
|
||||
}}
|
||||
aria-label="Sign Out"
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/data-browser';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import { OptionBase } from '@/ui/v2/Option';
|
||||
@@ -118,6 +118,7 @@ export default function BaseColumnForm({
|
||||
variant="inline"
|
||||
className="col-span-8 py-3"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<ControlledAutocomplete
|
||||
@@ -272,6 +273,7 @@ export default function BaseColumnForm({
|
||||
error={Boolean(errors.comment)}
|
||||
variant="inline"
|
||||
className="col-span-8 py-3"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import type { BaseForeignKeyFormValues } from '@/components/data-browser/BaseForeignKeyForm';
|
||||
import type { DatabaseColumn } from '@/types/data-browser';
|
||||
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import type { DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||
import LinkIcon from '@/ui/v2/icons/LinkIcon';
|
||||
@@ -2,7 +2,7 @@ import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/data-browser';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import Option from '@/ui/v2/Option';
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ControlledSelectProps } from '@/components/common/ControlledSelect';
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { NormalizedQueryDataRow } from '@/types/data-browser';
|
||||
import type { NormalizedQueryDataRow } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import type { ForwardedRef, PropsWithoutRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
@@ -1,5 +1,5 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { NormalizedQueryDataRow } from '@/types/data-browser';
|
||||
import type { NormalizedQueryDataRow } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import { useFormContext, useFormState, useWatch } from 'react-hook-form';
|
||||
import type { BaseForeignKeyFormValues } from './BaseForeignKeyForm';
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import DatabaseRecordInputGroup from '@/components/data-browser/DatabaseRecordInputGroup';
|
||||
import DatabaseRecordInputGroup from '@/components/dataBrowser/DatabaseRecordInputGroup';
|
||||
import type {
|
||||
ColumnInsertOptions,
|
||||
DataBrowserGridColumn,
|
||||
} from '@/types/data-browser';
|
||||
} from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import { baseColumnValidationSchema } from '@/components/data-browser/BaseColumnForm';
|
||||
import type { DatabaseTable, ForeignKeyRelation } from '@/types/data-browser';
|
||||
import { baseColumnValidationSchema } from '@/components/dataBrowser/BaseColumnForm';
|
||||
import type { DatabaseTable, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import { useEffect } from 'react';
|
||||
@@ -88,6 +88,7 @@ function NameInput() {
|
||||
error={Boolean(errors.name)}
|
||||
variant="inline"
|
||||
className="col-span-8 py-3"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
import ControlledAutocomplete from '@/components/common/ControlledAutocomplete';
|
||||
import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import type { ColumnType, ForeignKeyRelation } from '@/types/data-browser';
|
||||
import type { ColumnType, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import type { ButtonProps } from '@/ui/v2/Button';
|
||||
import type { CheckboxProps } from '@/ui/v2/Checkbox';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
@@ -70,6 +70,7 @@ function NameInput({ index }: FieldArrayInputProps) {
|
||||
}
|
||||
},
|
||||
})}
|
||||
autoComplete="off"
|
||||
aria-label="Name"
|
||||
placeholder="Enter name"
|
||||
hideEmptyHelperText
|
||||
@@ -82,7 +82,7 @@ export default function ColumnEditorTable() {
|
||||
startIcon={<PlusIcon />}
|
||||
size="small"
|
||||
>
|
||||
Add column
|
||||
Add Column
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ForeignKeyRelation } from '@/types/data-browser';
|
||||
import type { ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||
import LinkIcon from '@/ui/v2/icons/LinkIcon';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import type { BaseForeignKeyFormValues } from '@/components/data-browser/BaseForeignKeyForm';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/data-browser';
|
||||
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import InputLabel from '@/ui/v2/InputLabel';
|
||||
@@ -1,5 +1,5 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/data-browser';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import { identityTypes } from '@/utils/dataBrowser/postgresqlConstants';
|
||||
import { useMemo } from 'react';
|
||||
@@ -1,5 +1,5 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { DatabaseColumn } from '@/types/data-browser';
|
||||
import type { DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import { useMemo } from 'react';
|
||||
import { useFormState, useWatch } from 'react-hook-form';
|
||||
@@ -0,0 +1,129 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { ColumnAutocompleteProps } from './ColumnAutocomplete';
|
||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||
|
||||
export default {
|
||||
title: 'Data Browser / ColumnAutocomplete',
|
||||
component: ColumnAutocomplete,
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
type: 'code',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ColumnAutocomplete>;
|
||||
|
||||
const defaultParameters = {
|
||||
nextRouter: {
|
||||
path: '/[workspaceSlug]/[appSlug]/database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
asPath: '/workspace/app/database/browser/default/public/users',
|
||||
query: {
|
||||
workspaceSlug: 'workspace',
|
||||
appSlug: 'app',
|
||||
dataSourceSlug: 'default',
|
||||
schemaSlug: 'public',
|
||||
tableSlug: 'books',
|
||||
},
|
||||
},
|
||||
msw: {
|
||||
handlers: [tableQuery, hasuraMetadataQuery],
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof ColumnAutocomplete> = function Template(
|
||||
args: ColumnAutocompleteProps,
|
||||
) {
|
||||
const [submittedValues, setSubmittedValues] = useState<string>('');
|
||||
|
||||
const form = useForm<{ firstReference: string; secondReference: string }>({
|
||||
defaultValues: {
|
||||
firstReference: null,
|
||||
secondReference: null,
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(values: {
|
||||
firstReference: string;
|
||||
secondReference: string;
|
||||
}) {
|
||||
setSubmittedValues(JSON.stringify(values, null, 2));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<ColumnAutocomplete
|
||||
{...args}
|
||||
name="firstReference"
|
||||
label="First Reference"
|
||||
onChange={(_event, newValue) =>
|
||||
form.setValue('firstReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onInitialized={(newValue) => {
|
||||
form.setValue('firstReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ColumnAutocomplete
|
||||
{...args}
|
||||
name="secondReference"
|
||||
label="Second Reference"
|
||||
onChange={(_event, newValue) =>
|
||||
form.setValue('secondReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onInitialized={(newValue) => {
|
||||
form.setValue('secondReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="justify-self-start">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<Text component="pre" className="!font-mono !text-gray-700">
|
||||
{submittedValues || 'The form has not been submitted yet.'}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
};
|
||||
Basic.parameters = defaultParameters;
|
||||
|
||||
export const DefaultValue = Template.bind({});
|
||||
DefaultValue.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
value: 'author.id',
|
||||
};
|
||||
DefaultValue.parameters = defaultParameters;
|
||||
|
||||
export const DisabledRelationships = Template.bind({});
|
||||
DisabledRelationships.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
disableRelationships: true,
|
||||
};
|
||||
DisabledRelationships.parameters = defaultParameters;
|
||||
@@ -0,0 +1,33 @@
|
||||
import customClaimsQuery from '@/utils/msw/mocks/graphql/customClaimsQuery';
|
||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||
import { render, screen } from '@/utils/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { test, vi } from 'vitest';
|
||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||
|
||||
const server = setupServer(tableQuery, hasuraMetadataQuery, customClaimsQuery);
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should render a combobox', () => {
|
||||
render(
|
||||
<ColumnAutocomplete
|
||||
schema="public"
|
||||
table="books"
|
||||
label="Column Autocomplete"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: /column autocomplete/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Note: Network requests don't go through in tests, so we can't test the
|
||||
// autocomplete functionality for now.
|
||||
@@ -0,0 +1,406 @@
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import useMetadataQuery from '@/hooks/dataBrowser/useMetadataQuery';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
import { AutocompletePopper } from '@/ui/v2/Autocomplete';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import ArrowLeftIcon from '@/ui/v2/icons/ArrowLeftIcon';
|
||||
import type { InputProps } from '@/ui/v2/Input';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import List from '@/ui/v2/List';
|
||||
import { OptionBase } from '@/ui/v2/Option';
|
||||
import { OptionGroupBase } from '@/ui/v2/OptionGroup';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import getTruncatedText from '@/utils/common/getTruncatedText';
|
||||
import type { AutocompleteGroupedOption } from '@mui/base/AutocompleteUnstyled';
|
||||
import { useAutocomplete } from '@mui/base/AutocompleteUnstyled';
|
||||
import type { AutocompleteRenderGroupParams } from '@mui/material/Autocomplete';
|
||||
import { autocompleteClasses } from '@mui/material/Autocomplete';
|
||||
import type {
|
||||
ForwardedRef,
|
||||
HTMLAttributes,
|
||||
PropsWithoutRef,
|
||||
SyntheticEvent,
|
||||
} from 'react';
|
||||
import { forwardRef, useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { UseAsyncValueOptions } from './useAsyncValue';
|
||||
import useAsyncValue from './useAsyncValue';
|
||||
import type { UseColumnGroupsOptions } from './useColumnGroups';
|
||||
import useColumnGroups from './useColumnGroups';
|
||||
|
||||
export interface ColumnAutocompleteProps
|
||||
extends Omit<PropsWithoutRef<InputProps>, 'onChange'> {
|
||||
/**
|
||||
* Schema where the `table` is located.
|
||||
*/
|
||||
schema: string;
|
||||
/**
|
||||
* Table to get the columns from.
|
||||
*/
|
||||
table: string;
|
||||
/**
|
||||
* Function to be called when the value changes.
|
||||
*/
|
||||
onChange?: (
|
||||
event: SyntheticEvent,
|
||||
value: {
|
||||
value: string;
|
||||
columnMetadata?: Record<string, any>;
|
||||
disableReset?: boolean;
|
||||
},
|
||||
) => void;
|
||||
/**
|
||||
* Function to be called when the input is asynchronously initialized.
|
||||
*/
|
||||
onInitialized?: UseAsyncValueOptions['onInitialized'];
|
||||
/**
|
||||
* Class name to be applied to the root element.
|
||||
*/
|
||||
rootClassName?: string;
|
||||
/**
|
||||
* Determines if the autocomplete should allow relationships.
|
||||
*/
|
||||
disableRelationships?: UseColumnGroupsOptions['disableRelationships'];
|
||||
}
|
||||
|
||||
function renderGroup(params: AutocompleteRenderGroupParams) {
|
||||
return (
|
||||
<li key={params.key}>
|
||||
<OptionGroupBase>{params.group}</OptionGroupBase>
|
||||
|
||||
<List>{params.children}</List>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOption(
|
||||
option: AutocompleteOption<string>,
|
||||
optionProps: HTMLAttributes<HTMLLIElement>,
|
||||
) {
|
||||
return (
|
||||
<OptionBase
|
||||
{...optionProps}
|
||||
className="grid grid-flow-col items-baseline justify-start justify-items-start gap-1.5"
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
|
||||
{option.group === 'columns' && (
|
||||
<InlineCode>{option.metadata?.type || option.value}</InlineCode>
|
||||
)}
|
||||
</OptionBase>
|
||||
);
|
||||
}
|
||||
|
||||
function ColumnAutocomplete(
|
||||
{
|
||||
rootClassName,
|
||||
schema: defaultSchema,
|
||||
table: defaultTable,
|
||||
value: externalValue,
|
||||
disableRelationships,
|
||||
onChange,
|
||||
onInitialized,
|
||||
...props
|
||||
}: ColumnAutocompleteProps,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeRelationship, setActiveRelationship] = useState<{
|
||||
schema: string;
|
||||
table: string;
|
||||
name: string;
|
||||
}>();
|
||||
const selectedSchema = activeRelationship?.schema || defaultSchema;
|
||||
const selectedTable = activeRelationship?.table || defaultTable;
|
||||
|
||||
const {
|
||||
data: tableData,
|
||||
status: tableStatus,
|
||||
error: tableError,
|
||||
isFetching: isTableFetching,
|
||||
} = useTableQuery([`default.${selectedSchema}.${selectedTable}`], {
|
||||
schema: selectedSchema,
|
||||
table: selectedTable,
|
||||
preventRowFetching: true,
|
||||
queryOptions: { refetchOnWindowFocus: false },
|
||||
});
|
||||
|
||||
const {
|
||||
data: metadata,
|
||||
status: metadataStatus,
|
||||
error: metadataError,
|
||||
isFetching: isMetadataFetching,
|
||||
} = useMetadataQuery([`default.metadata`], {
|
||||
queryOptions: { refetchOnWindowFocus: false },
|
||||
});
|
||||
|
||||
const {
|
||||
initialized,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
selectedColumn,
|
||||
setSelectedColumn,
|
||||
selectedRelationships,
|
||||
setSelectedRelationships,
|
||||
relationshipDotNotation,
|
||||
activeRelationship: asyncActiveRelationship,
|
||||
} = useAsyncValue({
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
initialValue: externalValue as string,
|
||||
isTableLoading: tableStatus === 'loading' || isTableFetching,
|
||||
isMetadataLoading: metadataStatus === 'loading' || isMetadataFetching,
|
||||
tableData,
|
||||
metadata,
|
||||
onInitialized,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setActiveRelationship(asyncActiveRelationship);
|
||||
}, [asyncActiveRelationship]);
|
||||
|
||||
function isOptionEqualToValue(
|
||||
option: AutocompleteOption,
|
||||
value: NonNullable<string | AutocompleteOption>,
|
||||
) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return option.value === value;
|
||||
}
|
||||
|
||||
return option.value === value.value && option.custom === value.custom;
|
||||
}
|
||||
|
||||
function handleChange(
|
||||
event: SyntheticEvent,
|
||||
value: NonNullable<string | AutocompleteOption>,
|
||||
) {
|
||||
if (typeof value === 'string' || Array.isArray(value) || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('group' in value && value.group === 'columns') {
|
||||
setSelectedColumn(value);
|
||||
setOpen(false);
|
||||
setInputValue(value.value);
|
||||
|
||||
onChange?.(event, {
|
||||
value:
|
||||
selectedRelationships.length > 0
|
||||
? [relationshipDotNotation, value.value].join('.')
|
||||
: value.value,
|
||||
columnMetadata: value.metadata,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValue('');
|
||||
setSelectedColumn(null);
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
value.metadata?.target,
|
||||
]);
|
||||
}
|
||||
|
||||
const options = useColumnGroups({
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
tableData,
|
||||
metadata,
|
||||
disableRelationships,
|
||||
});
|
||||
|
||||
const {
|
||||
popupOpen,
|
||||
anchorEl,
|
||||
setAnchorEl,
|
||||
getRootProps,
|
||||
getInputLabelProps,
|
||||
getInputProps,
|
||||
getListboxProps,
|
||||
getOptionProps,
|
||||
groupedOptions,
|
||||
} = useAutocomplete({
|
||||
open,
|
||||
inputValue,
|
||||
options,
|
||||
id: props?.name,
|
||||
openOnFocus: !props.disabled,
|
||||
disableCloseOnSelect: true,
|
||||
value: selectedColumn,
|
||||
onClose: () => setOpen(false),
|
||||
groupBy: (option) => option.group,
|
||||
isOptionEqualToValue,
|
||||
onChange: handleChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...getRootProps()} className={rootClassName}>
|
||||
<Input
|
||||
{...props}
|
||||
ref={ref}
|
||||
fullWidth
|
||||
slotProps={{
|
||||
...(props.slotProps || {}),
|
||||
label: getInputLabelProps(),
|
||||
input: { ...(props.slotProps?.input || {}), ref: setAnchorEl },
|
||||
inputRoot: {
|
||||
...getInputProps(),
|
||||
className: twMerge(
|
||||
Boolean(selectedColumn) || Boolean(relationshipDotNotation)
|
||||
? '!pl-0'
|
||||
: null,
|
||||
props.slotProps?.inputRoot?.className,
|
||||
),
|
||||
},
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
}}
|
||||
error={Boolean(tableError || metadataError) || props.error}
|
||||
helperText={
|
||||
String(tableError || metadataError || '') || props.helperText
|
||||
}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
value={inputValue}
|
||||
startAdornment={
|
||||
selectedColumn || relationshipDotNotation ? (
|
||||
<Text
|
||||
className={twMerge(
|
||||
'!ml-2 lg:max-w-[200px] flex-shrink-0 truncate',
|
||||
props.disabled && 'text-greyscaleGrey',
|
||||
)}
|
||||
>
|
||||
<span className="text-greyscaleGrey">{defaultTable}</span>.
|
||||
{relationshipDotNotation && (
|
||||
<>
|
||||
<span className="hidden lg:inline">
|
||||
{getTruncatedText(relationshipDotNotation, 15, 'end')}.
|
||||
</span>
|
||||
|
||||
<span className="inline lg:hidden">
|
||||
{getTruncatedText(relationshipDotNotation, 35, 'end')}.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
endAdornment={
|
||||
tableStatus === 'loading' ||
|
||||
metadataStatus === 'loading' ||
|
||||
!initialized ? (
|
||||
<ActivityIndicator className="mr-2" delay={500} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutocompletePopper
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
modifiers={[{ name: 'offset', options: { offset: [0, 10] } }]}
|
||||
placement="bottom-start"
|
||||
open={popupOpen}
|
||||
anchorEl={anchorEl}
|
||||
style={{ width: anchorEl?.parentElement?.clientWidth }}
|
||||
>
|
||||
<div className={autocompleteClasses.paper}>
|
||||
<div className="px-3 py-2.5 border-b-1 border-greyscale-100 grid grid-flow-col gap-2 justify-start items-center">
|
||||
{selectedRelationships.length > 0 && (
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
setInputValue('');
|
||||
setSelectedColumn(null);
|
||||
setSelectedRelationships((activeRelationships) =>
|
||||
activeRelationships.slice(0, -1),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Text className="truncate direction-rtl text-left">
|
||||
<span className="!text-greyscaleMedium">{defaultTable}</span>
|
||||
|
||||
{relationshipDotNotation && (
|
||||
<>
|
||||
<span className="hidden lg:inline">
|
||||
.{getTruncatedText(relationshipDotNotation, 20, 'start')}
|
||||
</span>
|
||||
|
||||
<span className="inline lg:hidden">
|
||||
.{relationshipDotNotation}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{(tableStatus === 'loading' ||
|
||||
metadataStatus === 'loading' ||
|
||||
!initialized) && (
|
||||
<div className="p-2">
|
||||
<ActivityIndicator label="Loading..." />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupedOptions.length > 0 && (
|
||||
<List
|
||||
{...getListboxProps()}
|
||||
className={autocompleteClasses.listbox}
|
||||
>
|
||||
{(
|
||||
groupedOptions as AutocompleteGroupedOption<
|
||||
typeof options[number]
|
||||
>[]
|
||||
).map((optionGroup) =>
|
||||
renderGroup({
|
||||
key: `${optionGroup.key}`,
|
||||
group: optionGroup.group,
|
||||
children: optionGroup.options.map((option, index) =>
|
||||
renderOption(
|
||||
option,
|
||||
getOptionProps({
|
||||
option,
|
||||
index: optionGroup.index + index,
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
|
||||
{groupedOptions.length === 0 && Boolean(anchorEl) && (
|
||||
<Text className={autocompleteClasses.noOptions}>No options</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutocompletePopper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(ColumnAutocomplete);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ColumnAutocomplete';
|
||||
export { default } from './ColumnAutocomplete';
|
||||
@@ -0,0 +1,302 @@
|
||||
import type { FetchMetadataReturnType } from '@/hooks/dataBrowser/useMetadataQuery';
|
||||
import type { FetchTableReturnType } from '@/hooks/dataBrowser/useTableQuery';
|
||||
import type { HasuraMetadataTable } from '@/types/dataBrowser';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface UseAsyncValueOptions {
|
||||
/**
|
||||
* Selected schema to be used to determine the initial value.
|
||||
*/
|
||||
selectedSchema?: string;
|
||||
/**
|
||||
* Selected table to be used to determine the initial value.
|
||||
*/
|
||||
selectedTable?: string;
|
||||
/**
|
||||
* Initial value to be used before the async value is loaded.
|
||||
*/
|
||||
initialValue?: string;
|
||||
/**
|
||||
* Determines whether or not the table data is loading.
|
||||
*/
|
||||
isTableLoading?: boolean;
|
||||
/**
|
||||
* Determines whether or not the metadata is loading.
|
||||
*/
|
||||
isMetadataLoading?: boolean;
|
||||
/**
|
||||
* Table data to be used to determine the initial value.
|
||||
*/
|
||||
tableData?: FetchTableReturnType;
|
||||
/**
|
||||
* Metadata to be used to determine the initial value.
|
||||
*/
|
||||
metadata?: FetchMetadataReturnType;
|
||||
/**
|
||||
* Function to be called when the input is asynchronously initialized.
|
||||
*/
|
||||
onInitialized?: (value: {
|
||||
value: string;
|
||||
columnMetadata?: Record<string, any>;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function useAsyncValue({
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
initialValue,
|
||||
isTableLoading,
|
||||
isMetadataLoading,
|
||||
tableData,
|
||||
metadata,
|
||||
onInitialized,
|
||||
}: UseAsyncValueOptions) {
|
||||
const currentTablePath = `${selectedSchema}.${selectedTable}`;
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
// We might not going to have the most up-to-date table data because the
|
||||
// relationship is loaded asynchronously, so we need to make sure we don't
|
||||
// look for the column in a stale table when initializing
|
||||
const [asyncTablePath, setAsyncTablePath] = useState(currentTablePath);
|
||||
const [remainingColumnPath, setRemainingColumnPath] = useState(
|
||||
initialValue?.split('.') || [],
|
||||
);
|
||||
const [selectedRelationships, setSelectedRelationships] = useState<
|
||||
{ schema: string; table: string; name: string }[]
|
||||
>([]);
|
||||
const relationshipDotNotation = selectedRelationships
|
||||
.map((relationship) => relationship.name)
|
||||
.join('.');
|
||||
const [selectedColumn, setSelectedColumn] =
|
||||
useState<AutocompleteOption>(null);
|
||||
const activeRelationship =
|
||||
selectedRelationships[selectedRelationships.length - 1];
|
||||
|
||||
useEffect(() => {
|
||||
if (remainingColumnPath?.length > 0 || initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInitialized(true);
|
||||
|
||||
if (!selectedColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
onInitialized?.({
|
||||
value:
|
||||
selectedRelationships.length > 0
|
||||
? [relationshipDotNotation, selectedColumn.value].join('.')
|
||||
: selectedColumn.value,
|
||||
columnMetadata: selectedColumn.metadata,
|
||||
});
|
||||
}, [
|
||||
initialized,
|
||||
onInitialized,
|
||||
relationshipDotNotation,
|
||||
remainingColumnPath?.length,
|
||||
selectedColumn,
|
||||
selectedRelationships.length,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
remainingColumnPath?.length !== 1 ||
|
||||
isTableLoading ||
|
||||
!tableData?.columns ||
|
||||
asyncTablePath !== currentTablePath
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [activeColumn] = remainingColumnPath;
|
||||
|
||||
// If there is a single column in the path, it means that we can look for it
|
||||
// in the table columns
|
||||
if (
|
||||
!tableData?.columns.some((column) => column.column_name === activeColumn)
|
||||
) {
|
||||
setRemainingColumnPath([]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedColumn({
|
||||
value: activeColumn,
|
||||
label: activeColumn,
|
||||
group: 'columns',
|
||||
metadata: tableData.columns.find(
|
||||
(column) => column.column_name === activeColumn,
|
||||
),
|
||||
});
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
setInputValue(activeColumn);
|
||||
}, [
|
||||
remainingColumnPath,
|
||||
isTableLoading,
|
||||
tableData?.columns,
|
||||
asyncTablePath,
|
||||
currentTablePath,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
remainingColumnPath.length < 2 ||
|
||||
isTableLoading ||
|
||||
isMetadataLoading ||
|
||||
!tableData?.columns ||
|
||||
asyncTablePath !== currentTablePath
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataMap = metadata.tables.reduce(
|
||||
(map, metadataTable) =>
|
||||
map.set(
|
||||
`${metadataTable.table.schema}.${metadataTable.table.name}`,
|
||||
metadataTable,
|
||||
),
|
||||
new Map<string, HasuraMetadataTable>(),
|
||||
);
|
||||
|
||||
const [nextPath] = remainingColumnPath.slice(
|
||||
0,
|
||||
remainingColumnPath.length - 1,
|
||||
);
|
||||
|
||||
const tableMetadata = metadataMap.get(`${selectedSchema}.${selectedTable}`);
|
||||
const currentRelationship = [
|
||||
...(tableMetadata?.object_relationships || []),
|
||||
...(tableMetadata?.array_relationships || []),
|
||||
].find(({ name }) => name === nextPath);
|
||||
|
||||
if (!currentRelationship) {
|
||||
setRemainingColumnPath([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
foreign_key_constraint_on: metadataConstraint,
|
||||
manual_configuration: metadataManualConfiguration,
|
||||
} = currentRelationship.using || {};
|
||||
|
||||
if (metadataManualConfiguration) {
|
||||
setAsyncTablePath(
|
||||
`${metadataManualConfiguration.remote_table.schema}.${metadataManualConfiguration.remote_table.name}`,
|
||||
);
|
||||
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
{
|
||||
schema: metadataManualConfiguration.remote_table.schema || 'public',
|
||||
table: metadataManualConfiguration.remote_table.name,
|
||||
name: nextPath,
|
||||
},
|
||||
]);
|
||||
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// In some cases the metadata already contains the schema and table name
|
||||
if (metadataConstraint && typeof metadataConstraint !== 'string') {
|
||||
setAsyncTablePath(
|
||||
`${metadataConstraint.table.schema || 'public'}.${
|
||||
metadataConstraint.table.name
|
||||
}`,
|
||||
);
|
||||
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
{
|
||||
schema: metadataConstraint.table.schema || 'public',
|
||||
table: metadataConstraint.table.name,
|
||||
name: nextPath,
|
||||
},
|
||||
]);
|
||||
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const foreignKeyRelation = tableData?.foreignKeyRelations?.find(
|
||||
({ columnName }) => {
|
||||
const normalizedColumnName = columnName.replace(/"/g, '');
|
||||
const { foreign_key_constraint_on, manual_configuration } =
|
||||
currentRelationship.using || {};
|
||||
|
||||
if (!foreign_key_constraint_on && !manual_configuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (manual_configuration) {
|
||||
return Object.keys(manual_configuration.column_mapping).includes(
|
||||
normalizedColumnName,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof foreign_key_constraint_on === 'string') {
|
||||
return foreign_key_constraint_on === normalizedColumnName;
|
||||
}
|
||||
|
||||
return foreign_key_constraint_on.column === normalizedColumnName;
|
||||
},
|
||||
);
|
||||
|
||||
if (!foreignKeyRelation) {
|
||||
setRemainingColumnPath([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedSchema = foreignKeyRelation.referencedSchema?.replace(
|
||||
/(\\"|")/g,
|
||||
'',
|
||||
);
|
||||
const normalizedTable = foreignKeyRelation.referencedTable?.replace(
|
||||
/(\\"|")/g,
|
||||
'',
|
||||
);
|
||||
|
||||
setAsyncTablePath(`${normalizedSchema || 'public'}.${normalizedTable}`);
|
||||
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
{
|
||||
schema: normalizedSchema || 'public',
|
||||
table: normalizedTable,
|
||||
name: nextPath,
|
||||
},
|
||||
]);
|
||||
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
}, [
|
||||
currentTablePath,
|
||||
asyncTablePath,
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
metadata?.tables,
|
||||
tableData?.columns,
|
||||
tableData?.foreignKeyRelations,
|
||||
remainingColumnPath,
|
||||
isTableLoading,
|
||||
isMetadataLoading,
|
||||
]);
|
||||
|
||||
return {
|
||||
initialized,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
activeRelationship,
|
||||
selectedRelationships: initialized ? selectedRelationships : [],
|
||||
selectedColumn: initialized ? selectedColumn : null,
|
||||
setSelectedRelationships,
|
||||
setSelectedColumn,
|
||||
relationshipDotNotation:
|
||||
initialized && selectedRelationships?.length > 0
|
||||
? relationshipDotNotation
|
||||
: '',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { FetchMetadataReturnType } from '@/hooks/dataBrowser/useMetadataQuery';
|
||||
import type { FetchTableReturnType } from '@/hooks/dataBrowser/useTableQuery';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
|
||||
export interface UseColumnGroupsOptions {
|
||||
/**
|
||||
* Selected schema to be used to determines the column groups.
|
||||
*/
|
||||
selectedSchema?: string;
|
||||
/**
|
||||
* Selected table to be used to determine the column groups.
|
||||
*/
|
||||
selectedTable?: string;
|
||||
/**
|
||||
* Table data to be used to determine the column groups.
|
||||
*/
|
||||
tableData?: FetchTableReturnType;
|
||||
/**
|
||||
* Metadata to be used to determine the column groups.
|
||||
*/
|
||||
metadata?: FetchMetadataReturnType;
|
||||
/**
|
||||
* Determines whether or not to disable column groups.
|
||||
*/
|
||||
disableRelationships?: boolean;
|
||||
}
|
||||
|
||||
export default function useColumnGroups({
|
||||
selectedTable,
|
||||
selectedSchema,
|
||||
tableData,
|
||||
metadata,
|
||||
disableRelationships,
|
||||
}: UseColumnGroupsOptions) {
|
||||
const { columns, foreignKeyRelations } = tableData || {};
|
||||
|
||||
const columnTargetMap = foreignKeyRelations?.reduce(
|
||||
(map, currentRelation) =>
|
||||
map.set(currentRelation.columnName, {
|
||||
schema: currentRelation.referencedSchema || 'public',
|
||||
table: currentRelation.referencedTable,
|
||||
}),
|
||||
new Map<string, { schema: string; table: string }>(),
|
||||
);
|
||||
|
||||
const columnOptions: AutocompleteOption[] =
|
||||
columns?.map((column) => ({
|
||||
label: column.column_name,
|
||||
value: column.column_name,
|
||||
group: 'columns',
|
||||
metadata: column,
|
||||
})) || [];
|
||||
|
||||
if (disableRelationships) {
|
||||
return columnOptions;
|
||||
}
|
||||
|
||||
const { object_relationships, array_relationships } =
|
||||
metadata?.tables?.find(
|
||||
({ table: metadataTable }) =>
|
||||
metadataTable.name === selectedTable &&
|
||||
metadataTable.schema === selectedSchema,
|
||||
) || {};
|
||||
|
||||
const objectAndArrayRelationships = [
|
||||
...(object_relationships || []),
|
||||
...(array_relationships || []),
|
||||
].reduce((relationships, currentRelationship) => {
|
||||
const { foreign_key_constraint_on, manual_configuration } =
|
||||
currentRelationship?.using || {};
|
||||
|
||||
if (manual_configuration) {
|
||||
return [
|
||||
...relationships,
|
||||
...Object.keys(manual_configuration.column_mapping).map((column) => ({
|
||||
schema: manual_configuration.remote_table?.schema || 'public',
|
||||
table: manual_configuration.remote_table?.name,
|
||||
column,
|
||||
name: currentRelationship.name,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof foreign_key_constraint_on === 'string') {
|
||||
return [
|
||||
...relationships,
|
||||
{
|
||||
schema: selectedSchema,
|
||||
table: selectedTable,
|
||||
column: foreign_key_constraint_on,
|
||||
name: currentRelationship.name,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...relationships,
|
||||
{
|
||||
schema: foreign_key_constraint_on.table.schema,
|
||||
table: foreign_key_constraint_on.table.name,
|
||||
column: foreign_key_constraint_on.column,
|
||||
name: currentRelationship.name,
|
||||
},
|
||||
];
|
||||
}, [] as { schema: string; table: string; column: string; name: string }[]);
|
||||
|
||||
return [
|
||||
...columnOptions,
|
||||
...objectAndArrayRelationships.map((relationship) => ({
|
||||
label: relationship.name,
|
||||
value: relationship.name,
|
||||
group: 'relationships',
|
||||
metadata: {
|
||||
target: {
|
||||
schema: relationship.schema,
|
||||
table: relationship.table,
|
||||
column: relationship.column,
|
||||
...(columnTargetMap?.get(relationship.column) || {}),
|
||||
name: relationship.name,
|
||||
},
|
||||
},
|
||||
})),
|
||||
];
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type {
|
||||
BaseColumnFormProps,
|
||||
BaseColumnFormValues,
|
||||
} from '@/components/data-browser/BaseColumnForm';
|
||||
} from '@/components/dataBrowser/BaseColumnForm';
|
||||
import BaseColumnForm, {
|
||||
baseColumnValidationSchema,
|
||||
} from '@/components/data-browser/BaseColumnForm';
|
||||
} from '@/components/dataBrowser/BaseColumnForm';
|
||||
import useCreateColumnMutation from '@/hooks/dataBrowser/useCreateColumnMutation';
|
||||
import useTrackForeignKeyRelationsMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
@@ -1,11 +1,11 @@
|
||||
import type {
|
||||
BaseForeignKeyFormProps,
|
||||
BaseForeignKeyFormValues,
|
||||
} from '@/components/data-browser/BaseForeignKeyForm';
|
||||
} from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import {
|
||||
BaseForeignKeyForm,
|
||||
baseForeignKeyValidationSchema,
|
||||
} from '@/components/data-browser/BaseForeignKeyForm';
|
||||
} from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BaseRecordFormProps } from '@/components/data-browser/BaseRecordForm';
|
||||
import BaseRecordForm from '@/components/data-browser/BaseRecordForm';
|
||||
import type { BaseRecordFormProps } from '@/components/dataBrowser/BaseRecordForm';
|
||||
import BaseRecordForm from '@/components/dataBrowser/BaseRecordForm';
|
||||
import useCreateRecordMutation from '@/hooks/dataBrowser/useCreateRecordMutation';
|
||||
import type { ColumnInsertOptions } from '@/types/data-browser';
|
||||
import type { ColumnInsertOptions } from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { createDynamicValidationSchema } from '@/utils/dataBrowser/validationSchemaHelpers';
|
||||
@@ -1,14 +1,14 @@
|
||||
import type {
|
||||
BaseTableFormProps,
|
||||
BaseTableFormValues,
|
||||
} from '@/components/data-browser/BaseTableForm';
|
||||
} from '@/components/dataBrowser/BaseTableForm';
|
||||
import BaseTableForm, {
|
||||
baseTableValidationSchema,
|
||||
} from '@/components/data-browser/BaseTableForm';
|
||||
} from '@/components/dataBrowser/BaseTableForm';
|
||||
import useCreateTableMutation from '@/hooks/dataBrowser/useCreateTableMutation';
|
||||
import useTrackForeignKeyRelationMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
|
||||
import useTrackTableMutation from '@/hooks/dataBrowser/useTrackTableMutation';
|
||||
import type { DatabaseTable } from '@/types/data-browser';
|
||||
import type { DatabaseTable } from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
@@ -6,8 +6,8 @@ import DataGridNumericCell from '@/components/common/DataGridNumericCell';
|
||||
import DataGridTextCell from '@/components/common/DataGridTextCell';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import DataBrowserEmptyState from '@/components/data-browser/DataBrowserEmptyState';
|
||||
import DataBrowserGridControls from '@/components/data-browser/DataBrowserGridControls';
|
||||
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
|
||||
import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls';
|
||||
import useDeleteColumnWithToastMutation from '@/hooks/dataBrowser/useDeleteColumnMutation/useDeleteColumnWithToastMutation';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import type { UpdateRecordVariables } from '@/hooks/dataBrowser/useUpdateRecordMutation';
|
||||
@@ -17,7 +17,7 @@ import useTablePath from '@/hooks/useTablePath';
|
||||
import type {
|
||||
DataBrowserGridColumn,
|
||||
NormalizedQueryDataRow,
|
||||
} from '@/types/data-browser';
|
||||
} from '@/types/dataBrowser';
|
||||
import KeyIcon from '@/ui/v2/icons/KeyIcon';
|
||||
import normalizeDefaultValue from '@/utils/dataBrowser/normalizeDefaultValue';
|
||||
import {
|
||||
@@ -348,7 +348,7 @@ export default function DataBrowserGrid({
|
||||
description={
|
||||
<span>
|
||||
Schema{' '}
|
||||
<InlineCode className="max-h-[32px] bg-gray-200 bg-opacity-80 px-1.5 text-sm">
|
||||
<InlineCode className="bg-gray-200 bg-opacity-80 px-1.5 text-sm">
|
||||
{metadata.schema || schemaSlug}
|
||||
</InlineCode>{' '}
|
||||
does not exist.
|
||||
@@ -365,7 +365,7 @@ export default function DataBrowserGrid({
|
||||
description={
|
||||
<span>
|
||||
Table{' '}
|
||||
<InlineCode className="max-h-[32px] bg-gray-200 bg-opacity-80 px-1.5 text-sm">
|
||||
<InlineCode className="bg-gray-200 bg-opacity-80 px-1.5 text-sm">
|
||||
{metadata.schema || schemaSlug}.{metadata.table || tableSlug}
|
||||
</InlineCode>{' '}
|
||||
does not exist.
|
||||
@@ -4,7 +4,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
||||
import useDeleteRecordMutation from '@/hooks/dataBrowser/useDeleteRecordMutation';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import useDataGridConfig from '@/hooks/useDataGridConfig';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import Chip from '@/ui/Chip';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DataBrowserSidebarProps } from '@/components/data-browser/DataBrowserSidebar';
|
||||
import DataBrowserSidebar from '@/components/data-browser/DataBrowserSidebar';
|
||||
import type { DataBrowserSidebarProps } from '@/components/dataBrowser/DataBrowserSidebar';
|
||||
import DataBrowserSidebar from '@/components/dataBrowser/DataBrowserSidebar';
|
||||
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
|
||||
import ProjectLayout from '@/components/layout/ProjectLayout';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
@@ -8,6 +9,7 @@ import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAn
|
||||
import FloatingActionButton from '@/ui/FloatingActionButton';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Chip from '@/ui/v2/Chip';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
@@ -17,6 +19,7 @@ import LockIcon from '@/ui/v2/icons/LockIcon';
|
||||
import PencilIcon from '@/ui/v2/icons/PencilIcon';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import TrashIcon from '@/ui/v2/icons/TrashIcon';
|
||||
import UsersIcon from '@/ui/v2/icons/UsersIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
@@ -194,6 +197,40 @@ function DataBrowserSidebarContent({
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditPermissionClick(
|
||||
schema: string,
|
||||
table: string,
|
||||
disabled?: boolean,
|
||||
) {
|
||||
openDrawer('EDIT_PERMISSIONS', {
|
||||
title: (
|
||||
<span className="inline-grid grid-flow-col gap-2 items-center">
|
||||
Permissions
|
||||
<InlineCode className="!text-sm+ font-normal text-greyscaleMedium">
|
||||
{table}
|
||||
</InlineCode>
|
||||
<Chip label="Preview" size="small" color="info" component="span" />
|
||||
</span>
|
||||
),
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'lg:w-[65%] lg:max-w-7xl',
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
onSubmit: async () => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${schema}.${table}`,
|
||||
]);
|
||||
await refetch();
|
||||
},
|
||||
disabled,
|
||||
schema,
|
||||
table,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-1">
|
||||
{schemas && schemas.length > 0 && (
|
||||
@@ -318,9 +355,7 @@ function DataBrowserSidebarContent({
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
disabled={
|
||||
tablePath === removableTable || isGitHubConnected
|
||||
}
|
||||
disabled={tablePath === removableTable}
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
@@ -329,7 +364,6 @@ function DataBrowserSidebarContent({
|
||||
!isSelected &&
|
||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
||||
)}
|
||||
disabled={isGitHubConnected}
|
||||
>
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
@@ -339,44 +373,84 @@ function DataBrowserSidebarContent({
|
||||
menu
|
||||
PaperProps={{ className: 'w-52' }}
|
||||
>
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
openDrawer('EDIT_TABLE', {
|
||||
title: 'Edit Table',
|
||||
payload: {
|
||||
onSubmit: async () => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
|
||||
]);
|
||||
await refetch();
|
||||
},
|
||||
schema: table.table_schema,
|
||||
table,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<PencilIcon className="h-4 w-4 text-gray-700" />
|
||||
{isGitHubConnected ? (
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
true,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon className="h-4 w-4 text-gray-700" />
|
||||
|
||||
<span>Edit Table</span>
|
||||
</Dropdown.Item>
|
||||
<span>View Permissions</span>
|
||||
</Dropdown.Item>
|
||||
) : (
|
||||
[
|
||||
<Dropdown.Item
|
||||
key="edit-table"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
openDrawer('EDIT_TABLE', {
|
||||
title: 'Edit Table',
|
||||
payload: {
|
||||
onSubmit: async () => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
|
||||
]);
|
||||
await refetch();
|
||||
},
|
||||
schema: table.table_schema,
|
||||
table,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<PencilIcon className="h-4 w-4 text-gray-700" />
|
||||
|
||||
<Divider component="li" />
|
||||
<span>Edit Table</span>
|
||||
</Dropdown.Item>,
|
||||
<Divider
|
||||
key="edit-table-separator"
|
||||
component="li"
|
||||
/>,
|
||||
<Dropdown.Item
|
||||
key="edit-permissions"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon className="h-4 w-4 text-gray-700" />
|
||||
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium text-red"
|
||||
onClick={() =>
|
||||
handleDeleteTableClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4 text-red" />
|
||||
<span>Edit Permissions</span>
|
||||
</Dropdown.Item>,
|
||||
<Divider
|
||||
key="edit-permissions-separator"
|
||||
component="li"
|
||||
/>,
|
||||
<Dropdown.Item
|
||||
key="delete-table"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium text-red"
|
||||
onClick={() =>
|
||||
handleDeleteTableClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4 text-red" />
|
||||
|
||||
<span>Delete Table</span>
|
||||
</Dropdown.Item>
|
||||
<span>Delete Table</span>
|
||||
</Dropdown.Item>,
|
||||
]
|
||||
)}
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import ReadOnlyToggle from '@/components/common/ReadOnlyToggle';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import KeyIcon from '@/ui/v2/icons/KeyIcon';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Option from '@/ui/v2/Option';
|
||||
@@ -127,7 +127,7 @@ export default function DatabaseRecordInputGroup({
|
||||
<span>{columnId}</span>
|
||||
</span>
|
||||
|
||||
<InlineCode>
|
||||
<InlineCode className="h-[18px]">
|
||||
{specificType}
|
||||
{maxLength ? `(${maxLength})` : null}
|
||||
</InlineCode>
|
||||
@@ -204,7 +204,7 @@ export default function DatabaseRecordInputGroup({
|
||||
multiline={isMultiline}
|
||||
rows={5}
|
||||
autoFocus={index === 0 && autoFocusFirstInput}
|
||||
componentsProps={{
|
||||
slotProps={{
|
||||
label: commonLabelProps,
|
||||
inputRoot: { step: 1 },
|
||||
}}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type {
|
||||
BaseColumnFormProps,
|
||||
BaseColumnFormValues,
|
||||
} from '@/components/data-browser/BaseColumnForm';
|
||||
} from '@/components/dataBrowser/BaseColumnForm';
|
||||
import BaseColumnForm, {
|
||||
baseColumnValidationSchema,
|
||||
} from '@/components/data-browser/BaseColumnForm';
|
||||
} from '@/components/dataBrowser/BaseColumnForm';
|
||||
import useTrackForeignKeyRelationMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
|
||||
import useUpdateColumnMutation from '@/hooks/dataBrowser/useUpdateColumnMutation';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -1,12 +1,12 @@
|
||||
import type {
|
||||
BaseForeignKeyFormProps,
|
||||
BaseForeignKeyFormValues,
|
||||
} from '@/components/data-browser/BaseForeignKeyForm';
|
||||
} from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import {
|
||||
BaseForeignKeyForm,
|
||||
baseForeignKeyValidationSchema,
|
||||
} from '@/components/data-browser/BaseForeignKeyForm';
|
||||
import type { ForeignKeyRelation } from '@/types/data-browser';
|
||||
} from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import type { ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user