Compare commits
804 Commits
@nhost/rea
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d5d0a53e | ||
|
|
4e0b132d20 | ||
|
|
425476759f | ||
|
|
04784d880b | ||
|
|
130131c488 | ||
|
|
f0da84bbec | ||
|
|
5efa43aa2e | ||
|
|
2497194dcc | ||
|
|
5733162ed6 | ||
|
|
ab106c9492 | ||
|
|
4d2aac807c | ||
|
|
a659760724 | ||
|
|
13086bcae3 | ||
|
|
86459468be | ||
|
|
34cec77ceb | ||
|
|
abfb42651a | ||
|
|
ec584181cc | ||
|
|
70b31358bc | ||
|
|
521f418f8c | ||
|
|
8851416e7a | ||
|
|
f98f5a4bca | ||
|
|
650a605b61 | ||
|
|
422e1bbeae | ||
|
|
367e86abd2 | ||
|
|
7e172d6352 | ||
|
|
e786a6fa84 | ||
|
|
f899f4000d | ||
|
|
ecd27f34d6 | ||
|
|
9f488d2739 | ||
|
|
fac066c0cd | ||
|
|
fdc56e9611 | ||
|
|
7b11f343ac | ||
|
|
04d39bef90 | ||
|
|
83e21f879f | ||
|
|
8e26cdb5ed | ||
|
|
4dc1a5ded3 | ||
|
|
b3f6c732dd | ||
|
|
a63342d0bd | ||
|
|
4913ff7a8b | ||
|
|
99cbbbcbf9 | ||
|
|
3a11b6a8fa | ||
|
|
be4b26c65d | ||
|
|
33df3c842d | ||
|
|
a5bba46b59 | ||
|
|
1358a41dc4 | ||
|
|
2b7cf59159 | ||
|
|
083c65b775 | ||
|
|
1c940469fb | ||
|
|
e2bf1118f9 | ||
|
|
9a1ad43370 | ||
|
|
e2b79b5ece | ||
|
|
c47d47ac9c | ||
|
|
926590acb5 | ||
|
|
90e8843314 | ||
|
|
aa5b360932 | ||
|
|
daa4b8b2ad | ||
|
|
a1c5c97a59 | ||
|
|
b338793d6d | ||
|
|
b1fb4b2400 | ||
|
|
f75e023672 | ||
|
|
8e78c1ff00 | ||
|
|
9cbb0b2986 | ||
|
|
363a3b92e5 | ||
|
|
6a078fc972 | ||
|
|
1091e9674a | ||
|
|
9738108d58 | ||
|
|
65951e1d1d | ||
|
|
b4af994a58 | ||
|
|
c6347e10bc | ||
|
|
278a641bc1 | ||
|
|
3320ddd8c8 | ||
|
|
bc9eff6e41 | ||
|
|
258c608882 | ||
|
|
ae84f269d4 | ||
|
|
0327250b19 | ||
|
|
7f56eabd24 | ||
|
|
be110df83a | ||
|
|
361e648daf | ||
|
|
8a72e20e3d | ||
|
|
125ec390ca | ||
|
|
7cc788a373 | ||
|
|
2a04bc9e5d | ||
|
|
f7c2148ace | ||
|
|
78d35eed09 | ||
|
|
c5ff53c622 | ||
|
|
d21714d169 | ||
|
|
0d16ad41b8 | ||
|
|
82c328eeda | ||
|
|
d991cd8c7e | ||
|
|
e469628ebe | ||
|
|
856bc0a4bb | ||
|
|
9b1fb1ce28 | ||
|
|
a4d16f1835 | ||
|
|
3db8644075 | ||
|
|
7f667f6acb | ||
|
|
685dc6c1e4 | ||
|
|
6f7f2b0a65 | ||
|
|
6d0167b33f | ||
|
|
3ffb60f0ae | ||
|
|
97ced73a3c | ||
|
|
39c86cea25 | ||
|
|
d2d590db7e | ||
|
|
3bdbefc015 | ||
|
|
79081b43c2 | ||
|
|
a4b541f100 | ||
|
|
4523020c33 | ||
|
|
2e2248fd44 | ||
|
|
63358eb80b | ||
|
|
ded674fab6 | ||
|
|
85f2f28902 | ||
|
|
b8e9ad831e | ||
|
|
4e0c5dd1d3 | ||
|
|
b874109c6d | ||
|
|
21b926cc07 | ||
|
|
c35cd47d97 | ||
|
|
8dcd801c7c | ||
|
|
e3199be749 | ||
|
|
284b31e036 | ||
|
|
e7593c7de8 | ||
|
|
e6d862ac1b | ||
|
|
f73672372f | ||
|
|
7f12b98d94 | ||
|
|
d79b66314d | ||
|
|
2a58266592 | ||
|
|
44c2c5467d | ||
|
|
142752cb79 | ||
|
|
b05236a23c | ||
|
|
11a46a0db1 | ||
|
|
cedff501d6 | ||
|
|
7c426dafb2 | ||
|
|
57e7f794f5 | ||
|
|
d4b6cb0acf | ||
|
|
5d0cf8814b | ||
|
|
96cf17bbeb | ||
|
|
ed1a8d458e | ||
|
|
8077495c18 | ||
|
|
b617ec7186 | ||
|
|
bb2da11dd4 | ||
|
|
94fa824e7d | ||
|
|
32d1ee124f | ||
|
|
138bf9eb5a | ||
|
|
d8d9310e0b | ||
|
|
67b2c044b8 | ||
|
|
0b7790ca83 | ||
|
|
55267c680e | ||
|
|
4d856f557f | ||
|
|
64c579cf8c | ||
|
|
eae65c715b | ||
|
|
9e69f9f235 | ||
|
|
8b127fbb62 | ||
|
|
86ba2081ec | ||
|
|
7c2c31082a | ||
|
|
60f705b033 | ||
|
|
ea34635eb2 | ||
|
|
2004687044 | ||
|
|
bd025d43ca | ||
|
|
87a05f7374 | ||
|
|
798f147db7 | ||
|
|
62b7fd2376 | ||
|
|
1ee021b4a3 | ||
|
|
6e61dce297 | ||
|
|
bd744e52dc | ||
|
|
85723d740b | ||
|
|
36e79e7b32 | ||
|
|
f61264b319 | ||
|
|
e84d9d2576 | ||
|
|
ea69d4f0f1 | ||
|
|
212d58bee5 | ||
|
|
c3d6b7beec | ||
|
|
5d5d8ef4f3 | ||
|
|
deb61fe97c | ||
|
|
04d36154b0 | ||
|
|
203cfb10b9 | ||
|
|
9690f871fa | ||
|
|
74a6b93971 | ||
|
|
dd4c0d2430 | ||
|
|
83f2ca5cde | ||
|
|
0c49e757c8 | ||
|
|
e90a9d7696 | ||
|
|
00a06466f5 | ||
|
|
8ca9f76cb2 | ||
|
|
78113dd62a | ||
|
|
adb0ee82c6 | ||
|
|
a41bb6cae6 | ||
|
|
1c59c363ee | ||
|
|
1d99f26fec | ||
|
|
49edb0e627 | ||
|
|
f011e71ae1 | ||
|
|
00c363f808 | ||
|
|
0b2f749ae9 | ||
|
|
cf62a1e6e3 | ||
|
|
8df84d782f | ||
|
|
f0deffafe1 | ||
|
|
a291da661d | ||
|
|
66c3193bc9 | ||
|
|
ac7be49cef | ||
|
|
fa79b77093 | ||
|
|
5823947933 | ||
|
|
333837fb57 | ||
|
|
7fae68f6cf | ||
|
|
f2751f4bac | ||
|
|
089acbbe70 | ||
|
|
6e08a82f49 | ||
|
|
6899ef3b39 | ||
|
|
cad3686364 | ||
|
|
8f2c002715 | ||
|
|
b70d61198f | ||
|
|
d29af2ce6f | ||
|
|
cdc992b888 | ||
|
|
205a20de87 | ||
|
|
b092b8fe08 | ||
|
|
2d40cbf624 | ||
|
|
7b591e8c4c | ||
|
|
72b425a5bc | ||
|
|
971ff92ab4 | ||
|
|
b7f801874d | ||
|
|
ff69f30e47 | ||
|
|
cc1932492d | ||
|
|
f45037e79f | ||
|
|
48658e2925 | ||
|
|
b90bb6b924 | ||
|
|
de61f45bd5 | ||
|
|
fd11e5ca2c | ||
|
|
7839c786ef | ||
|
|
a2bcd6a4b6 | ||
|
|
2cd5b26e0e | ||
|
|
559611af70 | ||
|
|
ffb45f5a49 | ||
|
|
451e80ac12 | ||
|
|
c9f8e523f2 | ||
|
|
331ba03768 | ||
|
|
611b26bc7d | ||
|
|
a446c3efca | ||
|
|
24424ae4dc | ||
|
|
2a5b705c26 | ||
|
|
7f3a32d386 | ||
|
|
11fa442aa8 | ||
|
|
5764f46d99 | ||
|
|
78d501801b | ||
|
|
cc8cc8d45d | ||
|
|
61fc83996b | ||
|
|
9ddb37e9bb | ||
|
|
262828f9a1 | ||
|
|
12f9726ad7 | ||
|
|
845937b552 | ||
|
|
f777a3380a | ||
|
|
5081372cab | ||
|
|
82212345c8 | ||
|
|
32d3f167c5 | ||
|
|
3d5f1ea922 | ||
|
|
97841ee5e8 | ||
|
|
4f3a615ebe | ||
|
|
8e8197691c | ||
|
|
e10389ecf6 | ||
|
|
cbdf6affec | ||
|
|
d19406e694 | ||
|
|
cffc5dc65b | ||
|
|
2b5cb58553 | ||
|
|
7459a9413e | ||
|
|
56871cc9f7 | ||
|
|
8f4d66e52d | ||
|
|
315a820073 | ||
|
|
ca57ad2cbd | ||
|
|
40259344eb | ||
|
|
4749f60a08 | ||
|
|
ac1888514d | ||
|
|
49b4af439b | ||
|
|
61e03d6c70 | ||
|
|
bec0fce497 | ||
|
|
c01568a7dd | ||
|
|
e934216a82 | ||
|
|
701d6b8c84 | ||
|
|
e158e2440a | ||
|
|
fbaa657001 | ||
|
|
559db6d0ec | ||
|
|
4c844930f1 | ||
|
|
3ef503ff81 | ||
|
|
bfcfd236ea | ||
|
|
bfa7033506 | ||
|
|
78c29fcf0e | ||
|
|
f1b934ed22 | ||
|
|
914369c53f | ||
|
|
af379b967e | ||
|
|
c3efb7ec84 | ||
|
|
27cbd48c8c | ||
|
|
236996a903 | ||
|
|
5d0936bb93 | ||
|
|
733c212f2d | ||
|
|
8b47549189 | ||
|
|
3c9c1025ce | ||
|
|
3e46d3873c | ||
|
|
4cf8820d72 | ||
|
|
02a11184fb | ||
|
|
7214d47cc7 | ||
|
|
238b77baad | ||
|
|
81b8e538b4 | ||
|
|
563a37e58d | ||
|
|
bff23720ee | ||
|
|
02cbaeffd2 | ||
|
|
9eb814c79a | ||
|
|
ebc5913bb3 | ||
|
|
4fe4a16964 | ||
|
|
92c475b7a7 | ||
|
|
679b34b031 | ||
|
|
d3186aefbd | ||
|
|
fdecac9d69 | ||
|
|
5077283028 | ||
|
|
f5f662aad1 | ||
|
|
735b779af7 | ||
|
|
4418d6abcf | ||
|
|
049e315c30 | ||
|
|
764597538b | ||
|
|
c8aea785cc | ||
|
|
e0e44b2ff4 | ||
|
|
12280f7c87 | ||
|
|
732a4f40ca | ||
|
|
d67fd599e4 | ||
|
|
a41231927a | ||
|
|
42ec665950 | ||
|
|
7225712a30 | ||
|
|
6593fdd9bb | ||
|
|
40039fece5 | ||
|
|
e5fcfb3cd5 | ||
|
|
218ec314fb | ||
|
|
9367e91d45 | ||
|
|
06c640be2c | ||
|
|
ae45be9816 | ||
|
|
ec4be590d8 | ||
|
|
5c51653aa0 | ||
|
|
7348c15ad1 | ||
|
|
44831e32a7 | ||
|
|
ee0f837762 | ||
|
|
e040979e91 | ||
|
|
68100d63b9 | ||
|
|
9b800046d7 | ||
|
|
807d8574b6 | ||
|
|
77028e4eef | ||
|
|
e0d32aab33 | ||
|
|
75c4c8ae36 | ||
|
|
1d90639e46 | ||
|
|
765b398b21 | ||
|
|
30aae1557c | ||
|
|
a3efc1d131 | ||
|
|
612d754965 | ||
|
|
b2e5f30379 | ||
|
|
3b3e83a218 | ||
|
|
0d5231f1a1 | ||
|
|
1a8332a3ca | ||
|
|
7418105de2 | ||
|
|
425d485f85 | ||
|
|
d8d25b3ea0 | ||
|
|
320513f6f5 | ||
|
|
b37053376d | ||
|
|
c21ba4aebd | ||
|
|
58948c50d4 | ||
|
|
ae324f67fa | ||
|
|
acabf2b168 | ||
|
|
73cb65b9be | ||
|
|
5e7c8395c2 | ||
|
|
c2837209e6 | ||
|
|
638710ea29 | ||
|
|
a79fddbafb | ||
|
|
ab6a8f2add | ||
|
|
69a5661bcf | ||
|
|
0886118f9d | ||
|
|
34fc08ca7c | ||
|
|
153de22713 | ||
|
|
bf4a1f6c2a | ||
|
|
2a67d0f872 | ||
|
|
b156c7b72e | ||
|
|
b484b04ae2 | ||
|
|
2e55c7f46a | ||
|
|
2d983e6ab1 | ||
|
|
df5b4302c3 | ||
|
|
828aed2df9 | ||
|
|
310df10892 | ||
|
|
555fba4400 | ||
|
|
885d10620a | ||
|
|
a8370f5aaa | ||
|
|
bd07905846 | ||
|
|
47a2164549 | ||
|
|
a96c79de00 | ||
|
|
596d0666fc | ||
|
|
9aaa407d29 | ||
|
|
1767b2f105 | ||
|
|
c99c5c4191 | ||
|
|
d845da2503 | ||
|
|
9f1ba1686c | ||
|
|
48b09a58ff | ||
|
|
2169908883 | ||
|
|
ed16c8b5de | ||
|
|
c618503376 | ||
|
|
f306c3940c | ||
|
|
ef125216bb | ||
|
|
fb43fefb5c | ||
|
|
73744c90f0 | ||
|
|
9fbea9787e | ||
|
|
e5f54bc197 | ||
|
|
10a6ae4853 | ||
|
|
d6ca1c7cfd | ||
|
|
bb85a95eda | ||
|
|
e84acf4692 | ||
|
|
2f20a70a28 | ||
|
|
e622ca0d83 | ||
|
|
819e1e97dc | ||
|
|
7c1cca0a43 | ||
|
|
0f51f4e868 | ||
|
|
97a6fcead9 | ||
|
|
b7c799d62c | ||
|
|
18b14b27fd | ||
|
|
67a867c93a | ||
|
|
0a1fb12467 | ||
|
|
78467ee348 | ||
|
|
c24eef0db9 | ||
|
|
2159b8171e | ||
|
|
8903e6abd9 | ||
|
|
7290260990 | ||
|
|
06529a1ea4 | ||
|
|
607d89e2aa | ||
|
|
0cca72311c | ||
|
|
a6525b6467 | ||
|
|
387be37b6e | ||
|
|
c8fd8bbcc7 | ||
|
|
bfb34bad00 | ||
|
|
666a75a233 | ||
|
|
3b050217df | ||
|
|
0ed4481615 | ||
|
|
ac3f12c878 | ||
|
|
65cabb089f | ||
|
|
2905beb0a1 | ||
|
|
83fee54460 | ||
|
|
82898b6dae | ||
|
|
500f76a38d | ||
|
|
5e1e80aa8b | ||
|
|
6d0a126907 | ||
|
|
1b7dcf2121 | ||
|
|
2b9205b6cf | ||
|
|
bdc4d4a88c | ||
|
|
45759c4d4c | ||
|
|
5f9886577a | ||
|
|
fa65496327 | ||
|
|
03777680c1 | ||
|
|
72c81207ff | ||
|
|
5ca2a394e8 | ||
|
|
e63b8da58a | ||
|
|
bf8543cd34 | ||
|
|
8a557bbd02 | ||
|
|
327e30b859 | ||
|
|
bbfaf9732b | ||
|
|
c064a53256 | ||
|
|
ebda86f1f0 | ||
|
|
8948be9d3d | ||
|
|
54e9b141f1 | ||
|
|
dba71483df | ||
|
|
77ef68232a | ||
|
|
8fbc7f9f95 | ||
|
|
ca9f0f6ae9 | ||
|
|
e819903f1b | ||
|
|
f780b17581 | ||
|
|
032c0bd217 | ||
|
|
5d278709cb | ||
|
|
3a012e089a | ||
|
|
7aed620e12 | ||
|
|
d9fd1a54a5 | ||
|
|
a19b85c8ac | ||
|
|
4e1aaca0ee | ||
|
|
34ef37cdce | ||
|
|
5d6b655cb1 | ||
|
|
074a0fa111 | ||
|
|
403d839fca | ||
|
|
4e3098240b | ||
|
|
dd0a5cf3c1 | ||
|
|
5187fd3a4b | ||
|
|
d8dfd6bf80 | ||
|
|
6ea6ad61db | ||
|
|
fd0b904ed4 | ||
|
|
8989e314a6 | ||
|
|
5b5a1219c5 | ||
|
|
07fda9bbb3 | ||
|
|
2fa828fef1 | ||
|
|
d5ec69ac37 | ||
|
|
4a7ede11e9 | ||
|
|
482ae4c4f1 | ||
|
|
08fe4cd65f | ||
|
|
5781721bca | ||
|
|
39de0063bf | ||
|
|
202b647234 | ||
|
|
51c163a268 | ||
|
|
6e802c9938 | ||
|
|
9a46104e37 | ||
|
|
655b317c39 | ||
|
|
d3ad7c9d4a | ||
|
|
09fc852c3a | ||
|
|
ece08d3efd | ||
|
|
3493442c2d | ||
|
|
632a79b9e4 | ||
|
|
4a4d85757a | ||
|
|
88a01004b7 | ||
|
|
73230eb35a | ||
|
|
27e1c90624 | ||
|
|
1cc53d550a | ||
|
|
22d3f71e02 | ||
|
|
010b816866 | ||
|
|
4a6e62e673 | ||
|
|
5cf9dd9bc2 | ||
|
|
27e74c10d7 | ||
|
|
bd807a5ee1 | ||
|
|
4093e03a13 | ||
|
|
29076d0304 | ||
|
|
ab83fa6b5e | ||
|
|
b20761e976 | ||
|
|
a445e5b786 | ||
|
|
90df6d81d8 | ||
|
|
aa85084675 | ||
|
|
07ad470c0c | ||
|
|
fa6b58a9c5 | ||
|
|
acf55376ba | ||
|
|
b0a9798b04 | ||
|
|
3952e87f01 | ||
|
|
b95ccf873d | ||
|
|
8d7f84b8da | ||
|
|
bd1b69bd75 | ||
|
|
84d5436634 | ||
|
|
2325766c1d | ||
|
|
2c355eaae4 | ||
|
|
9e26ed767e | ||
|
|
abdb6c56f4 | ||
|
|
3b75bfce27 | ||
|
|
f498190758 | ||
|
|
b4158fa513 | ||
|
|
3d1a177632 | ||
|
|
0675a213b5 | ||
|
|
a8ff383490 | ||
|
|
960d815f68 | ||
|
|
edf2b4e93f | ||
|
|
fe240542a4 | ||
|
|
c7752c0657 | ||
|
|
d1e2b1c75a | ||
|
|
bcdab66bf8 | ||
|
|
7636f40030 | ||
|
|
e643bd3620 | ||
|
|
311c7756d7 | ||
|
|
f967a2e596 | ||
|
|
4c4b253a71 | ||
|
|
0f5f8c0d90 | ||
|
|
37a7fc05d5 | ||
|
|
bf93d87b36 | ||
|
|
efb3dc7294 | ||
|
|
42bd7807b2 | ||
|
|
eea59bd202 | ||
|
|
7248eb733f | ||
|
|
fceb6a4a89 | ||
|
|
b10eca09a8 | ||
|
|
4799b65e96 | ||
|
|
067eb9d6a9 | ||
|
|
219d5ecdcf | ||
|
|
9073182d51 | ||
|
|
bdb5783e79 | ||
|
|
ece717d6e0 | ||
|
|
b135ef695c | ||
|
|
82b3353110 | ||
|
|
3f165a85e3 | ||
|
|
aa4018909f | ||
|
|
98397e3ccd | ||
|
|
911e7112c9 | ||
|
|
e62402ecfc | ||
|
|
9190dd726d | ||
|
|
ae093283d0 | ||
|
|
875327fbea | ||
|
|
3d5c34f4ce | ||
|
|
58c2a20532 | ||
|
|
6c90cb5024 | ||
|
|
7e37570587 | ||
|
|
87d225a840 | ||
|
|
7b0de27c80 | ||
|
|
564fc76195 | ||
|
|
2ed4f40c12 | ||
|
|
d67a023e21 | ||
|
|
c99d117d1c | ||
|
|
a497a6ba0a | ||
|
|
160cd08cc7 | ||
|
|
120151c40c | ||
|
|
9dc16f29b3 | ||
|
|
964fc5644a | ||
|
|
2f907fc68f | ||
|
|
fe6cadc2cd | ||
|
|
338c8e5a80 | ||
|
|
e6f3a1a39d | ||
|
|
a168faeb69 | ||
|
|
b1628c59b5 | ||
|
|
32a2f5db9a | ||
|
|
818a48f74d | ||
|
|
bed377d05f | ||
|
|
709a616cfa | ||
|
|
860e2d877c | ||
|
|
5c6b2f88b9 | ||
|
|
f151a0e872 | ||
|
|
4a84bbb410 | ||
|
|
fa3a50e323 | ||
|
|
398152358c | ||
|
|
34ae9046f3 | ||
|
|
a478689587 | ||
|
|
9dbc0607dc | ||
|
|
7455efdd53 | ||
|
|
d0aff6141f | ||
|
|
aed0c4f82a | ||
|
|
74d4276c1a | ||
|
|
1e98130aa1 | ||
|
|
52e9b510da | ||
|
|
ece197eb6b | ||
|
|
d14e112bff | ||
|
|
83884f04a5 | ||
|
|
977de21e86 | ||
|
|
462a60a8f8 | ||
|
|
9aa4371ef4 | ||
|
|
f0feddd83f | ||
|
|
0748cab125 | ||
|
|
27885491ee | ||
|
|
a36bdbf907 | ||
|
|
d3e8bb94ae | ||
|
|
645595ee43 | ||
|
|
4d82bc5609 | ||
|
|
fdf1e555d8 | ||
|
|
90c694cbba | ||
|
|
3262fa7b37 | ||
|
|
ab43fe567f | ||
|
|
b4c10f9f8a | ||
|
|
f4c6e7cfab | ||
|
|
72d1e94cb3 | ||
|
|
82d221a48d | ||
|
|
3fe46771b9 | ||
|
|
a1c487aa21 | ||
|
|
cf455608e2 | ||
|
|
5dac12dd41 | ||
|
|
2389b46e0d | ||
|
|
6fe2d22d0e | ||
|
|
0b439149e4 | ||
|
|
a9d7da8af7 | ||
|
|
3ecc21a45e | ||
|
|
aa19e85cdc | ||
|
|
26c650227d | ||
|
|
face99ccde | ||
|
|
49bcc525ad | ||
|
|
533563c893 | ||
|
|
cfe527307e | ||
|
|
1e36c6706d | ||
|
|
6e40b114fc | ||
|
|
77acf1385d | ||
|
|
cec7edd2d5 | ||
|
|
9dbbdb3121 | ||
|
|
79d2602648 | ||
|
|
b0363a4f4c | ||
|
|
17045b2018 | ||
|
|
c49cc11862 | ||
|
|
c83fe7d776 | ||
|
|
235b4c7405 | ||
|
|
c2c0fbd33a | ||
|
|
300e3f49e0 | ||
|
|
a95a77886b | ||
|
|
1f3f683202 | ||
|
|
4c67fd23c4 | ||
|
|
93d8d71e34 | ||
|
|
47bda15ff2 | ||
|
|
4563488b5d | ||
|
|
8fd35f3fea | ||
|
|
9c61c69a7b | ||
|
|
030ad4621e | ||
|
|
ee0b9b8edc | ||
|
|
c6fa8da6df | ||
|
|
dd9dedc226 | ||
|
|
5638a91240 | ||
|
|
cdefbdebee | ||
|
|
923abd3655 | ||
|
|
ef28540f9a | ||
|
|
d54e4cdd4e | ||
|
|
4a00963602 | ||
|
|
7ea9b890c8 | ||
|
|
f866120a65 | ||
|
|
472559276c | ||
|
|
2cdb13b3ef | ||
|
|
a41124c5e0 | ||
|
|
6ecffa81ae | ||
|
|
ea7b102c07 | ||
|
|
e9daf92830 | ||
|
|
9e4ad76e7f | ||
|
|
0fd65db563 | ||
|
|
146fbb84b9 | ||
|
|
b51c18fedb | ||
|
|
a5305e6b56 | ||
|
|
aa88ef2e5c | ||
|
|
ee6b3c9ac8 | ||
|
|
79fd86acc5 | ||
|
|
c2cbeddcb8 | ||
|
|
62b2de59d4 | ||
|
|
2a760593db | ||
|
|
9288873ce8 | ||
|
|
47014be8e3 | ||
|
|
49719f7a84 | ||
|
|
b3b64a3b74 | ||
|
|
3a56c12df4 | ||
|
|
5b15a4f235 | ||
|
|
83303017c3 | ||
|
|
e0739a5883 | ||
|
|
0a5a841cc8 | ||
|
|
3309835f06 | ||
|
|
32b221f944 | ||
|
|
e8a99badb8 | ||
|
|
1ea6e01963 | ||
|
|
958dec5dfe | ||
|
|
09257fbfb2 | ||
|
|
61e3497a13 | ||
|
|
a7b4e5606d | ||
|
|
34d77c9db1 | ||
|
|
4f1efd28a6 | ||
|
|
07a45fde0e | ||
|
|
9d0380eef3 | ||
|
|
ce3ec36b0a | ||
|
|
b62a9d19b5 | ||
|
|
c1472079c5 | ||
|
|
dd36971798 | ||
|
|
6199c1c555 | ||
|
|
f41fdc12af | ||
|
|
fc419ffa4d | ||
|
|
b7c102e876 | ||
|
|
873fc36e61 | ||
|
|
29743f0b71 | ||
|
|
d904ca2bbf | ||
|
|
80b22724de | ||
|
|
80e49f4459 | ||
|
|
b3d5ead508 | ||
|
|
77dcb8c964 | ||
|
|
3488da9dfd | ||
|
|
0e68a1fdfd | ||
|
|
8797b2bd17 | ||
|
|
5ef0b31573 | ||
|
|
86e5e0fb50 | ||
|
|
c2d589dd29 | ||
|
|
4b807d8134 | ||
|
|
ccdabb707f | ||
|
|
364bc87fd3 | ||
|
|
cc02902cbb | ||
|
|
0e838b9406 | ||
|
|
37ebf7d8e2 | ||
|
|
e23af24bdd | ||
|
|
90eb53cf19 | ||
|
|
7e516d7630 | ||
|
|
0861e41e70 | ||
|
|
057e7e2572 | ||
|
|
5a4e237a29 | ||
|
|
c7501c70ae | ||
|
|
6a45c1abad | ||
|
|
660d339e14 | ||
|
|
3dca08595d | ||
|
|
7c501c4e4f | ||
|
|
b9316bb668 | ||
|
|
5e1d5b737c | ||
|
|
bd4d0c2708 | ||
|
|
1d04ad6306 | ||
|
|
a4fa5f6f59 | ||
|
|
7e973d568a | ||
|
|
d81c52209b | ||
|
|
72744b3082 | ||
|
|
ff4efe2712 | ||
|
|
2982b90469 | ||
|
|
428a5df038 | ||
|
|
f79bf784b5 | ||
|
|
3b7449ac08 | ||
|
|
37bbfdb7ae | ||
|
|
eb570d2d09 | ||
|
|
c8c2a10b2d | ||
|
|
92c79eb2fb | ||
|
|
e70b45498d | ||
|
|
2e1ecfa731 | ||
|
|
8d323a7762 | ||
|
|
8aa0ff936a | ||
|
|
c6806d60c7 | ||
|
|
a13eb25ebc | ||
|
|
228d8a0686 | ||
|
|
0de1bc7ce3 | ||
|
|
6a94cad04b | ||
|
|
8643d25cc8 | ||
|
|
e820f11dda | ||
|
|
3555ab2b71 | ||
|
|
6e41d58131 | ||
|
|
6cf3beae1c | ||
|
|
022b76e784 | ||
|
|
2fbe88f806 | ||
|
|
9457bc32ca | ||
|
|
3de2639ae9 | ||
|
|
c43e549224 | ||
|
|
fc6fe5007b | ||
|
|
829febf33b | ||
|
|
ae99ba14b9 | ||
|
|
a158dc3a17 | ||
|
|
8420550990 | ||
|
|
156667cdbd | ||
|
|
7d388a8c91 | ||
|
|
dfa8776b2b | ||
|
|
1b9f15cb67 | ||
|
|
b683615269 | ||
|
|
a60ca2f6f5 | ||
|
|
14a2ead79f | ||
|
|
b625a6b4d4 | ||
|
|
fd12aa0a8d |
@@ -5,6 +5,5 @@
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ runs:
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8.5.1
|
||||
version: 8.6.2
|
||||
run_install: false
|
||||
- name: Get pnpm cache directory
|
||||
id: pnpm-cache-dir
|
||||
@@ -26,7 +26,7 @@ runs:
|
||||
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: ${{ runner.os }}-node-
|
||||
- name: Use Node.js 16
|
||||
- name: Use Node.js v18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
67
.github/workflows/changesets.yaml
vendored
67
.github/workflows/changesets.yaml
vendored
@@ -10,6 +10,7 @@ on:
|
||||
- '**.md'
|
||||
- '!.changeset/**'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
@@ -41,6 +42,7 @@ jobs:
|
||||
commit: 'chore: update versions'
|
||||
title: 'chore: update versions'
|
||||
publish: pnpm run release
|
||||
createGithubReleases: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -61,12 +63,39 @@ jobs:
|
||||
uses: ./.github/workflows/dashboard.yaml
|
||||
secrets: inherit
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
- name: Trigger a Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
|
||||
publish-docker:
|
||||
name: Publish to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
- version
|
||||
- publish-vercel
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
@@ -99,7 +128,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push to Docker Hub
|
||||
uses: docker/build-push-action@v4
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 90
|
||||
with:
|
||||
context: .
|
||||
file: ./dashboard/Dockerfile
|
||||
@@ -112,42 +141,6 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
- name: Create GitHub Release
|
||||
uses: taiki-e/create-gh-release-action@v1
|
||||
with:
|
||||
changelog: dashboard/CHANGELOG.md
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: ${{ env.DASHBOARD_PACKAGE }}@
|
||||
ref: refs/tags/${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
- name: Remove tag on failure
|
||||
if: failure()
|
||||
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
- name: Trigger a Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
|
||||
bump-cli:
|
||||
name: Bump Dashboard version in the Nhost CLI
|
||||
|
||||
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
BUILD: 'all'
|
||||
- name: Check if the pnpm lockfile changed
|
||||
id: changed-lockfile
|
||||
uses: tj-actions/changed-files@v36
|
||||
uses: tj-actions/changed-files@v37
|
||||
with:
|
||||
files: pnpm-lock.yaml
|
||||
# * Determine a pnpm filter argument for packages that have been modified.
|
||||
@@ -146,6 +146,7 @@ jobs:
|
||||
run: echo "NHOST_TEST_DASHBOARD_URL=https://${{ steps.fetch-dashboard-preview-url.outputs.preview_url }}" >> $GITHUB_ENV
|
||||
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
|
||||
- name: Run e2e tests
|
||||
timeout-minutes: 15
|
||||
run: pnpm --filter="${{ matrix.package.name }}" run e2e
|
||||
- id: file-name
|
||||
if: ${{ failure() }}
|
||||
|
||||
56
.github/workflows/codeql-analysis.yml
vendored
Normal file
56
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push: {}
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: '20 23 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -19,10 +19,8 @@ logs/
|
||||
coverage/
|
||||
dist/
|
||||
umd/
|
||||
lib/
|
||||
node_modules/
|
||||
tmp/
|
||||
.docz/
|
||||
.pnpm-store
|
||||
.turbo
|
||||
.env
|
||||
@@ -32,7 +30,6 @@ out/
|
||||
# Custom
|
||||
*.min.js
|
||||
*.map
|
||||
todo.md
|
||||
|
||||
# Config files that are not part of the repository root anymore. Should be removed in the future.
|
||||
/.eslintignore
|
||||
@@ -62,3 +59,7 @@ todo.md
|
||||
|
||||
# Nhost CLI data
|
||||
.nhost
|
||||
|
||||
# Nix
|
||||
.envrc
|
||||
.direnv/
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,5 +2,6 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"eslint.workingDirectories": ["./dashboard"]
|
||||
"eslint.workingDirectories": ["./dashboard"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
103
DEVELOPERS.md
103
DEVELOPERS.md
@@ -1,16 +1,37 @@
|
||||
# Developer guide
|
||||
# Developer Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
- This repository works with **Node 16**
|
||||
### Node.js v18
|
||||
|
||||
- We use [pnpm](https://pnpm.io/) as a package manager to speed up development and builds, and as a basis for our monorepo. You need to make sure it's installed on your machine. There are [several ways to install it](https://pnpm.io/installation), but the easiest way is with `npm`:
|
||||
_⚠️ Node.js v16 is also supported for the time being but support will be dropped in the near future_.
|
||||
|
||||
### [pnpm](https://pnpm.io/) package manager
|
||||
|
||||
The easiest way to install `pnpm` if it's not installed on your machine yet is to use `npm`:
|
||||
|
||||
```sh
|
||||
$ npm install -g pnpm
|
||||
```
|
||||
|
||||
- Our tests and examples use the Nhost CLI, to run the backend services locally. You can follow the installation instructions in [our documentation](https://docs.nhost.io/get-started/cli-workflow/install-cli).
|
||||
### [Nhost CLI](https://docs.nhost.io/cli)
|
||||
|
||||
- The CLI is primarily used for running the E2E tests
|
||||
- Please refer to the [installation guide](https://docs.nhost.io/get-started/cli-workflow/install-cli) if you have not installed it yet
|
||||
|
||||
## File Structure
|
||||
|
||||
The repository is organized as a monorepo, with the following structure (only relevant folders are shown):
|
||||
|
||||
```
|
||||
assets/ # Assets used in the README
|
||||
config/ # Configuration files for the monorepo
|
||||
dashboard/ # Dashboard
|
||||
docs/ # Documentation website
|
||||
examples/ # Example projects
|
||||
packages/ # Core packages
|
||||
integrations/ # These are packages that rely on the core packages
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
||||
@@ -31,25 +52,25 @@ $ pnpm install
|
||||
|
||||
### Development
|
||||
|
||||
Although package references are correctly updated on the fly for TypeScript, example projects won't
|
||||
see the changes because they are depending on the build output. To fix this, you can run packages
|
||||
in development mode.
|
||||
Although package references are correctly updated on the fly for TypeScript, example projects and the dashboard won't see the changes because they are depending on the build output. To fix this, you can run packages in development mode.
|
||||
|
||||
Running packages in development mode is as simple as:
|
||||
Running packages in development mode from the root folder is as simple as:
|
||||
|
||||
```sh
|
||||
$ pnpm dev
|
||||
```
|
||||
|
||||
Our packages are linked together using [PNPM's workspace](https://pnpm.io/workspaces) feature. Vite automatically detects changes in the dependencies and rebuilds everything, so that the changes are immediately reflected in the other packages.
|
||||
Our packages are linked together using [PNPM's workspace](https://pnpm.io/workspaces) feature. Next.js and Vite automatically detect changes in the dependencies and rebuild everything, so the changes will be reflected in the examples and the dashboard.
|
||||
|
||||
### Use examples
|
||||
**Note:** It's possible that Next.js or Vite throw an error when you run `pnpm dev`. Restarting the process should fix it.
|
||||
|
||||
### Use Examples
|
||||
|
||||
Examples are a great way to test your changes in practice. Make sure you've `pnpm dev` running in your terminal and then run an example.
|
||||
|
||||
Let's follow the instructions to run [react-apollo example](https://github.com/nhost/nhost/blob/main/examples/react-apollo/README.md).
|
||||
|
||||
## Run the documentation website locally
|
||||
## Edit Documentation
|
||||
|
||||
The easier way to contribute to our documentation is to go to the `docs` folder and follow the [instructions to start local development](https://github.com/nhost/nhost/blob/main/docs/README.md):
|
||||
|
||||
@@ -60,9 +81,9 @@ $ pnpm install
|
||||
$ pnpm start
|
||||
```
|
||||
|
||||
## Run test suites
|
||||
## Run Test Suites
|
||||
|
||||
### Unit tests
|
||||
### Unit Tests
|
||||
|
||||
You can run the unit tests with the following command from the repository root:
|
||||
|
||||
@@ -70,7 +91,7 @@ You can run the unit tests with the following command from the repository root:
|
||||
$ pnpm test
|
||||
```
|
||||
|
||||
### End-to-end tests
|
||||
### E2E Tests
|
||||
|
||||
Each package that defines end-to-end tests embeds their own Nhost configuration, that will be automatically when running the tests. As a result, you must make sure you are not running the Nhost CLI before running the tests.
|
||||
|
||||
@@ -83,24 +104,60 @@ $ pnpm e2e
|
||||
## Changesets
|
||||
|
||||
If you've made changes to the packages, you must describe those changes so that they can be reflected in the next release.
|
||||
We use [changesets](https://github.com/changesets/changesets) to support our versioning and release workflows. When you submit a pull request, a bot checks if some changesets are present, and if not, it directs you to add them.
|
||||
We use [changesets](https://github.com/changesets/changesets) to support our versioning and release workflows. When you submit a pull request, a bot checks if changesets are present, and if not, it asks you to add them.
|
||||
|
||||
The most comprehensive way to add a changeset is to run the following command in the repository root:
|
||||
To create a changeset, run the following command from the repository root:
|
||||
|
||||
```sh
|
||||
$ pnpm changeset
|
||||
```
|
||||
|
||||
This will create a file in the `.changeset` directory. You can edit it to give more details about the change you just made.
|
||||
This command will guide you through the process of creating a changeset. It will create a file in the `.changeset` directory.
|
||||
|
||||
You can take a look at the changeset documentation: [How to add a changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
|
||||
|
||||
## Committing changes
|
||||
### Selecting the Version
|
||||
|
||||
You'll notice that `git commit` takes a few seconds to run. We set a commit hook that scans the changes in the code, automatically generates documentation from the inline [TSDoc](https://tsdoc.org/) annotations, and adds these generated documentation files to the commit. They automatically update the [reference documentation](https://docs.nhost.io/reference).
|
||||
When you create a changeset, you will be asked to select the version of the package that you are bumping. The versioning scheme is as follows:
|
||||
|
||||
- **major**
|
||||
- For breaking changes (e.g: changing the function signature, etc.)
|
||||
- Should be avoided as much as possible as it will require users to update their code. Instead, consider supporting both the old and the new API simultaneously for a while.
|
||||
- For example: `v1.5.8` -> `v2.0.0`
|
||||
- **minor**
|
||||
- For new features (e.g: adding a new page to the dashboard, etc.)
|
||||
- For example: `v1.5.8` -> `v1.6.0`
|
||||
- **patch**
|
||||
- For bug fixes (e.g: fixing a typo, etc.)
|
||||
- For example: `v1.5.8` -> `v1.5.9`
|
||||
|
||||
<!-- ## Good practices
|
||||
- lint
|
||||
- prettier
|
||||
- documentation -->
|
||||
### Writing Good Changesets
|
||||
|
||||
A concise summary that describes the changes should be added to each PR. This summary will be used as the changeset description.
|
||||
|
||||
The following structure is used for describing changes:
|
||||
|
||||
- **The type of the change**:
|
||||
|
||||
- fix
|
||||
- feat
|
||||
- chore
|
||||
- docs
|
||||
|
||||
- **The scope of the change** (_broader scopes (e.g: dashboard, hasura-storage-js, etc.) are not recommended as GitHub Releases already contain which project is being bumped_):
|
||||
|
||||
- projects
|
||||
- deployments
|
||||
- deps
|
||||
- etc.
|
||||
|
||||
- **A short summary of the changes that were made**
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `fix(deployments): use correct timestamp for deployment details`
|
||||
- `chore(deps): bump @types/react to v18.2.8`
|
||||
- `feat(secrets): enable secrets`
|
||||
- etc.
|
||||
|
||||
You can always take a look at examples of changesets in the [GitHub Releases section](https://github.com/nhost/nhost/releases).
|
||||
|
||||
@@ -34,7 +34,7 @@ Nhost consists of open source software:
|
||||
- Authentication: [Hasura Auth](https://github.com/nhost/hasura-auth/)
|
||||
- Storage: [Hasura Storage](https://github.com/nhost/hasura-storage)
|
||||
- Serverless Functions: Node.js (JavaScript and TypeScript)
|
||||
- [Nhost CLI](https://docs.nhost.io/reference/cli) for local development
|
||||
- [Nhost CLI](https://docs.nhost.io/cli) for local development
|
||||
|
||||
## Architecture of Nhost
|
||||
|
||||
@@ -97,7 +97,7 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
|
||||
# Resources
|
||||
|
||||
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/reference/cli)
|
||||
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/cli)
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import baseLibConfig from './vite.lib.config'
|
||||
export default defineConfig({
|
||||
...baseLibConfig,
|
||||
optimizeDeps: {
|
||||
include: ['react/jsx-runtime']
|
||||
include: ['react/jsx-runtime'],
|
||||
exclude: ['react-hook-form']
|
||||
},
|
||||
plugins: [react({ jsxRuntime: 'classic' }), ...baseLibConfig.plugins]
|
||||
})
|
||||
|
||||
@@ -2,13 +2,14 @@ import '@fontsource/inter';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { NhostClient, NhostProvider } from '@nhost/nextjs';
|
||||
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 { createTheme } from '../src/components/ui/v2/createTheme';
|
||||
import '../src/styles/globals.css';
|
||||
import createTheme from '../src/theme/createTheme';
|
||||
|
||||
global.Buffer = Buffer;
|
||||
|
||||
@@ -56,5 +57,10 @@ export const decorators = [
|
||||
<Story />
|
||||
</NhostApolloProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostProvider nhost={new NhostClient({ subdomain: 'local' })}>
|
||||
<Story />
|
||||
</NhostProvider>
|
||||
),
|
||||
mswDecorator,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,383 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 04784d880: Fix graphite's default version
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5733162ed: feat: add settings and ui for graphite
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e2b79b5ec: chore: remove sharp from deps
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@7.0.1
|
||||
- @nhost/nextjs@2.0.1
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- bc9eff6e4: chore: remove support for using backendUrl when instantiating the Nhost client
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bc9eff6e4]
|
||||
- @nhost/nextjs@2.0.0
|
||||
- @nhost/react-apollo@7.0.0
|
||||
|
||||
## 0.21.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 97ced73a3: fix(dashboard): prevent dashboard from resolving secrets
|
||||
|
||||
## 0.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ed1a8d458: Update alert message on increasing PostgreSQL's volume capacity
|
||||
- 2e2248fd4: feat(dashboard): add SQL editor
|
||||
|
||||
## 0.20.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7c2c31082: feat: add support for users to delete their account
|
||||
- @nhost/react-apollo@6.0.1
|
||||
- @nhost/nextjs@1.13.40
|
||||
|
||||
## 0.20.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fa79b7709: chore(dashboard): tweaks and fixes to the service form and dialog
|
||||
- 8df84d782: fix(dashboard): allow resetting custom domains
|
||||
- @nhost/react-apollo@6.0.0
|
||||
- @nhost/nextjs@1.13.39
|
||||
|
||||
## 0.20.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 331ba0376: feat(dashboard): add postgres storage capacity modifier in the settings
|
||||
- b7f801874: feat(dashboard): add new settings page for custom domains
|
||||
|
||||
## 0.20.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.38
|
||||
|
||||
## 0.20.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e10389ecf: fix(dashboard): disable run tab when developing locally
|
||||
- @nhost/react-apollo@5.0.37
|
||||
|
||||
## 0.20.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c01568a7d: chore(dashboard): show alert to update oauth providers
|
||||
|
||||
## 0.20.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c3efb7ec8: feat(dashboard): query latest announcement from platform
|
||||
|
||||
## 0.20.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3e46d3873: chore: update link to node18 announcement
|
||||
|
||||
## 0.20.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.36
|
||||
- @nhost/nextjs@1.13.38
|
||||
|
||||
## 0.20.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 75c4c8ae3: feat(dashboard): make env value input multiline
|
||||
|
||||
## 0.20.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 425d485f8: fix(dashboard): make sure dedicated resources pricing follows total resources
|
||||
|
||||
## 0.20.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae324f67f: fix(dashboard): remove unused graphql fields
|
||||
|
||||
## 0.20.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- df5b4302c: chore(dashboard): remove run feature flag
|
||||
- bf4a1f6c2: feat(dashboard): fetch auth, postgres, hasura and storage versions from dashboard
|
||||
- 34fc08ca7: fix(dashboard/run): show correct private registry in service details
|
||||
- 885d10620: chore(dashboard): change feedback to contact us
|
||||
|
||||
## 0.20.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ed16c8b5d: feat(run): add a confirmation dialog when deleting a run service
|
||||
- 216990888: fix(run): center loading indicator when selecting a project
|
||||
|
||||
## 0.20.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9fbea9787: feat: add node18 announcement
|
||||
|
||||
## 0.20.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e84acf469: fix(run): handle subdomain undefined error when creating a new service
|
||||
|
||||
## 0.20.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b7c799d62: feat(run): add dialog to copy registry and URLs
|
||||
|
||||
## 0.20.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8903e6abd: fix(dashboard): show correct egress limit in usage stats
|
||||
|
||||
## 0.20.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 666a75a23: feat(dashboard): add functions execution time and egress volume to usage stats
|
||||
|
||||
## 0.20.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5e1e80aa8: fix(dashboard): show correct locales in user details
|
||||
- @nhost/react-apollo@5.0.35
|
||||
- @nhost/nextjs@1.13.37
|
||||
|
||||
## 0.20.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.34
|
||||
- @nhost/nextjs@1.13.36
|
||||
|
||||
## 0.20.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4a7ede11e: fix: distinguish files that were not uploaded
|
||||
- 202b64723: feat(nhost-run): add support for one-click-install run services
|
||||
- 074a0fa11: feat(dashboard): add settings toggle to enable/disable antivirus
|
||||
- @nhost/react-apollo@5.0.33
|
||||
- @nhost/nextjs@1.13.35
|
||||
|
||||
## 0.20.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b20761e97: feat(services): add pricing info and confirmation dialog
|
||||
- 90df6d81d: fix(services): handle null values when editing a service
|
||||
- aa8508467: fix: query service logs correctly
|
||||
feat: enable multiline support for environment value input
|
||||
|
||||
## 0.20.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8d7f84b8d: fix: make announcement adapt to theme
|
||||
|
||||
## 0.20.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3b75bfce2: fix: make announcement close properly
|
||||
- f49819075: fix: show correct values when dedicated resources are disabled
|
||||
|
||||
## 0.20.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e643bd362: fix(services): fix errors when config is null
|
||||
- bcdab66bf: feat: add annoucement for nhost run
|
||||
- f967a2e59: added note about storage not being able to be downsized
|
||||
- 311c7756d: chore(services): consistent naming for compute
|
||||
|
||||
## 0.20.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9073182d5: chore(dashboard): bump `turbo` to 1.10.11
|
||||
- ece717d6e: feat(logs): show services in the logs page
|
||||
- 82b335311: feat(metrics): change grafana link to point to the dashboards
|
||||
- b135ef695: fix(services): set command as optional and set min replicas to 0
|
||||
|
||||
## 0.20.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3d5c34f4c: fix(auth): fix users pagination limit
|
||||
|
||||
## 0.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c99d117d1: feat(services): add support for custom services
|
||||
|
||||
## 0.19.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- face99ccd: chore(deps): bump turbo version
|
||||
- cfe527307: style: tweak pull config warning in dark mode
|
||||
- a9d7da8af: chore(deps): update dependency @types/pluralize to ^0.0.30
|
||||
- 9aa4371ef: chore: add hasura-auth version 0.21.2
|
||||
- d14e112bf: chore(deps): update dependency prettier-plugin-tailwindcss to ^0.4.0
|
||||
- d3e8bb94a: chore(deps): update dependency vite-plugin-dts to v3
|
||||
|
||||
## 0.19.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.32
|
||||
- @nhost/nextjs@1.13.34
|
||||
|
||||
## 0.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9c61c69a7: chore(dashboard):add postgres 14.6-20230705-1 to the version selector
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 47bda15ff: feat(settings): add warning to pull config
|
||||
|
||||
## 0.18.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ee0b9b8ed: chore(dashboard):add hasura v2.28.2 and v2.29.0 to the version selector
|
||||
|
||||
## 0.17.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.31
|
||||
- @nhost/nextjs@1.13.33
|
||||
|
||||
## 0.17.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f866120a6: fix(users): use the password length from the config
|
||||
|
||||
## 0.17.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.30
|
||||
- @nhost/nextjs@1.13.32
|
||||
|
||||
## 0.17.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ea7b102c0: fix(pat): highlight expired tokens
|
||||
|
||||
## 0.17.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b3b64a3b7: chore(deps): bump `@types/react` to `v18.2.14` and `@types/react-dom` to `v18.2.6`
|
||||
- 32b221f94: chore(deps): bump `graphiql` to `v3`
|
||||
- 3a56c12df: chore(deps): bump `turbo` to `v1.10.6`
|
||||
- Updated dependencies [b3b64a3b7]
|
||||
- @nhost/react-apollo@5.0.29
|
||||
- @nhost/nextjs@1.13.31
|
||||
|
||||
## 0.17.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f41fdc12a: chore(deps): bump `turbo` to `1.10.5`
|
||||
- 6199c1c55: fix(projects): don't redirect to 404 page
|
||||
- Updated dependencies [07a45fde0]
|
||||
- @nhost/react-apollo@5.0.28
|
||||
- @nhost/nextjs@1.13.30
|
||||
|
||||
## 0.17.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 80b22724d: chore(deps): bump `@types/react` to `v18.2.13`, `@types/react-dom` to `v18.2.6` and `@storybook/testing-library` to `v0.2.0`
|
||||
|
||||
## 0.17.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cc02902cb: chore(docs): update environment variable documentation
|
||||
|
||||
## 0.17.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 660d339e1: fix(storybook): don't break storybook
|
||||
- 660d339e1: fix(tests): prevent warnings during tests
|
||||
- @nhost/react-apollo@5.0.27
|
||||
- @nhost/nextjs@1.13.29
|
||||
|
||||
## 0.17.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bd4d0c270: chore(dashboard):add postgres 14.6-20230613-1 to the version selector
|
||||
|
||||
## 0.17.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c8c2a10b2: fix(database): don't break the password reset flow
|
||||
- e70b45498: chore(deps): bump `@types/react` to `v18.2.12` and `@types/react-dom` to `v18.2.5`
|
||||
|
||||
## 0.17.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 842055099: chore(deps): bump `turbo` to `v1.10.3` and `pnpm` to `v8.6.2`
|
||||
- fd12aa0a8: chore(projects): remove the postgres password input from the project creation screen
|
||||
- 022b76e78: chore(deps): bump `@types/react` to `v18.2.11`
|
||||
- 3555ab2b7: chore(deps): bump `vitest` monorepo to `v0.32.0`
|
||||
- c43e54922: feat(backups): add download button to backups
|
||||
|
||||
## 0.17.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
FROM node:16-alpine AS pruner
|
||||
FROM node:18-alpine AS pruner
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo@1.10.1
|
||||
RUN yarn global add turbo@1.10.11
|
||||
COPY . .
|
||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||
|
||||
FROM node:16-alpine AS builder
|
||||
FROM node:18-alpine AS builder
|
||||
ARG TURBO_TOKEN
|
||||
ARG TURBO_TEAM
|
||||
|
||||
@@ -29,7 +29,7 @@ ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL __NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL_
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
|
||||
|
||||
RUN yarn global add pnpm@8.5.1
|
||||
RUN yarn global add pnpm@8.6.2
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-*.yaml .
|
||||
@@ -40,7 +40,7 @@ COPY turbo.json turbo.json
|
||||
COPY config/ config/
|
||||
RUN pnpm build:dashboard
|
||||
|
||||
FROM node:16-alpine AS runner
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
|
||||
@@ -3,10 +3,26 @@
|
||||
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).
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Then, build the packages that are used by the Nhost Dashboard:
|
||||
|
||||
```bash
|
||||
pnpm -w build
|
||||
```
|
||||
|
||||
Finally, run the development server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) to see the result in your browser.
|
||||
|
||||
## Environment
|
||||
|
||||
### Setup Environment Variables
|
||||
@@ -54,6 +70,12 @@ Components are documented using [Storybook](https://storybook.js.org/). To run S
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
By default, Storybook will run on port `6006`. You can change this by passing the `--port` flag:
|
||||
|
||||
```bash
|
||||
pnpm storybook --port 6007
|
||||
```
|
||||
|
||||
### General Environment Variables
|
||||
|
||||
| Name | Description |
|
||||
@@ -110,15 +132,19 @@ pnpm storybook
|
||||
| `@typescript-eslint/naming-convention` | Enforces a consistent naming convention. |
|
||||
| `no-restricted-imports` | Enforces absolute imports and consistent import paths for components from `src/components/ui` folder. |
|
||||
|
||||
### End-to-End Tests
|
||||
### Unit Tests
|
||||
|
||||
End-to-end tests are written using [Playwright](https://playwright.dev/). To run the tests, run the following command:
|
||||
Unit tests are written using [Vitest](https://vitest.dev/). To run the tests, run the following command:
|
||||
|
||||
```bash
|
||||
pnpm e2e
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Most of the tests require access to the Nhost test user. To run these tests, you need to set the following environment variables in `.env.test`:
|
||||
### End-to-End Tests
|
||||
|
||||
Most of the end-to-end tests require access to an Nhost test user and a live project. You can register a user and create a test project on the [Nhost Dashboard](https://app.nhost.io/).
|
||||
|
||||
Next, you need to create a project. Create a `.env.test` file with the following variables:
|
||||
|
||||
```
|
||||
NHOST_TEST_DASHBOARD_URL=<test_dashboard_url>
|
||||
@@ -128,3 +154,20 @@ NHOST_TEST_WORKSPACE_NAME=<test_workspace_name>
|
||||
NHOST_TEST_PROJECT_NAME=<test_project_name>
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET=<test_project_admin_secret>
|
||||
```
|
||||
|
||||
**Required Variables**:
|
||||
|
||||
- `NHOST_TEST_DASHBOARD_URL`: The URL to run the tests against (e.g: http://localhost:3000 or https://staging.app.nhost.io)
|
||||
- `NHOST_TEST_USER_EMAIL`: Email address of the test user that owns the test project
|
||||
- `NHOST_TEST_USER_PASSWORD`: Password of the test user that owns the test project
|
||||
- `NHOST_TEST_WORKSPACE_NAME`: Name of the workspace that contains the test project
|
||||
- `NHOST_TEST_PROJECT_NAME`: Name of the test project
|
||||
- `NHOST_TEST_PROJECT_ADMIN_SECRET`: Admin secret of the test project
|
||||
|
||||
Make sure to copy the workspace and project information from the [Nhost Dashboard](https://app.nhost.io/).
|
||||
|
||||
End-to-end tests are written using [Playwright](https://playwright.dev/). To run the tests, run the following command:
|
||||
|
||||
```bash
|
||||
pnpm e2e
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ test('should show a sidebar with menu items', async () => {
|
||||
const navLocator = page.getByRole('navigation', { name: /main navigation/i });
|
||||
await expect(navLocator).toBeVisible();
|
||||
await expect(navLocator.getByRole('list').getByRole('listitem')).toHaveCount(
|
||||
11,
|
||||
13,
|
||||
);
|
||||
await expect(
|
||||
navLocator.getByRole('link', { name: /overview/i }),
|
||||
|
||||
@@ -9,7 +9,7 @@ import { openProject } from '@/e2e/utils';
|
||||
import { chromium } from '@playwright/test';
|
||||
|
||||
async function globalTeardown() {
|
||||
const browser = await chromium.launch();
|
||||
const browser = await chromium.launch({ slowMo: 1000 });
|
||||
|
||||
const context = await browser.newContext({
|
||||
baseURL: TEST_DASHBOARD_URL,
|
||||
@@ -46,18 +46,23 @@ async function globalTeardown() {
|
||||
await hasuraPage.locator('a', { hasText: /data/i }).click();
|
||||
await hasuraPage.getByRole('link', { name: /sql/i }).click();
|
||||
|
||||
await hasuraPage.locator('#raw_sql > textarea').fill(`
|
||||
DO $$ DECLARE
|
||||
tablename text;
|
||||
BEGIN
|
||||
FOR tablename IN
|
||||
SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(tablename) || ' CASCADE';
|
||||
END LOOP;
|
||||
END $$;
|
||||
`);
|
||||
// Set the value of the Ace code editor using JavaScript evaluation in the browser context
|
||||
await hasuraPage.evaluate(() => {
|
||||
const editor = ace.edit('raw_sql');
|
||||
|
||||
editor.setValue(`
|
||||
DO $$ DECLARE
|
||||
tablename text;
|
||||
BEGIN
|
||||
FOR tablename IN
|
||||
SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(tablename) || ' CASCADE';
|
||||
END LOOP;
|
||||
END $$;
|
||||
`);
|
||||
});
|
||||
|
||||
await hasuraPage.getByRole('button', { name: /run!/i }).click();
|
||||
await hasuraPage.getByText(/sql executed!/i).waitFor();
|
||||
|
||||
14
dashboard/graphite.graphql.config.yaml
Normal file
14
dashboard/graphite.graphql.config.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
schema:
|
||||
- https://local.graphql.nhost.run/v1:
|
||||
headers:
|
||||
x-hasura-admin-secret: nhost-admin-secret
|
||||
generates:
|
||||
src/utils/__generated__/graphite.graphql.ts:
|
||||
documents:
|
||||
- 'src/gql/graphite/**/*.gql'
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
- 'typescript-react-apollo'
|
||||
config:
|
||||
withRefetchFn: true
|
||||
@@ -7,6 +7,7 @@ generates:
|
||||
documents:
|
||||
- 'src/**/*.graphql'
|
||||
- 'src/**/*.gql'
|
||||
- '!src/gql/graphite/**/*.gql'
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
|
||||
5
dashboard/hypertune.graphql
Normal file
5
dashboard/hypertune.graphql
Normal file
@@ -0,0 +1,5 @@
|
||||
query InitQuery {
|
||||
root {
|
||||
enableServices
|
||||
}
|
||||
}
|
||||
5
dashboard/hypertune.json
Normal file
5
dashboard/hypertune.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"projectId": 2596,
|
||||
"token": "U2FsdGVkX19+V8BJnVR0xLEC+42OW5qZl/A0i6beAaRmJoIhFh5Yf6eIKBzLbV9h",
|
||||
"outputDirectoryPath": "src/hypertune"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.17.8",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -11,22 +11,23 @@
|
||||
"lint": "next lint --max-warnings 0",
|
||||
"test": "vitest",
|
||||
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
|
||||
"nhost:dev": "nhost up",
|
||||
"codegen-graphite": "graphql-codegen --config graphite.graphql.config.yaml --errors-only",
|
||||
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
|
||||
"storybook": "start-storybook -p 6006 -s public",
|
||||
"build-storybook": "build-storybook",
|
||||
"e2e": "npx playwright@1.34.0 install --with-deps && playwright test"
|
||||
"install-browsers": "pnpm dlx playwright@1.31.0 install --with-deps",
|
||||
"e2e": "pnpm install-browsers && pnpm dlx playwright@1.31.0 test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.10",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/lang-sql": "^6.5.4",
|
||||
"@emotion/cache": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/server": "^11.4.0",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/inter": "^5.0.0",
|
||||
"@fontsource/roboto-mono": "^5.0.0",
|
||||
"@graphiql/react": "^0.17.0",
|
||||
"@graphiql/react": "^0.18.0",
|
||||
"@graphiql/toolkit": "^0.8.2",
|
||||
"@headlessui/react": "^1.6.5",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
@@ -44,16 +45,19 @@
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-table": "^8.5.30",
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
||||
"@uiw/codemirror-theme-github": "^4.21.20",
|
||||
"@uiw/react-codemirror": "^4.21.20",
|
||||
"analytics-node": "^6.2.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"generate-password": "^1.7.0",
|
||||
"graphiql": "^2.4.0",
|
||||
"graphiql": "^3.0.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^6.0.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"hypertune": "^1.4.4",
|
||||
"just-kebab-case": "^4.1.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "^12.3.1",
|
||||
@@ -61,16 +65,23 @@
|
||||
"node-pg-format": "^1.3.5",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-children-utilities": "^2.9.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^4.0.0",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"react-is": "18.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"react-resizable-layout": "^0.7.2",
|
||||
"react-table": "^7.8.0",
|
||||
"sharp": "^0.32.0",
|
||||
"recoil": "^0.7.7",
|
||||
"recoil-persist": "^5.1.0",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"shell-quote": "^1.8.1",
|
||||
"slugify": "^1.6.5",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^1.8.0",
|
||||
@@ -87,7 +98,7 @@
|
||||
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||
"@next/bundle-analyzer": "^12.3.1",
|
||||
"@playwright/test": "^1.34.0",
|
||||
"@playwright/test": "1.31.0",
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.14",
|
||||
"@storybook/addon-interactions": "^6.5.14",
|
||||
@@ -96,23 +107,28 @@
|
||||
"@storybook/builder-webpack5": "^6.5.14",
|
||||
"@storybook/manager-webpack5": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@storybook/testing-library": "^0.2.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/ace": "^0.0.48",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.2.8",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@types/pluralize": "^0.0.30",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/shell-quote": "^1.7.1",
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||
"@typescript-eslint/parser": "^6.15.0",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitest/coverage-c8": "^0.31.0",
|
||||
"@vitest/coverage-v8": "^0.32.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
@@ -136,7 +152,7 @@
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"prettier-plugin-tailwindcss": "^0.4.0",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"snake-case": "^3.0.4",
|
||||
@@ -146,7 +162,7 @@
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"vite": "^4.0.2",
|
||||
"vite-tsconfig-paths": "^4.0.3",
|
||||
"vitest": "^0.31.0"
|
||||
"vitest": "^0.32.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
BIN
dashboard/public/illustration-unbox.png
Normal file
BIN
dashboard/public/illustration-unbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
70
dashboard/src/components/common/ContactUs/ContactUs.tsx
Normal file
70
dashboard/src/components/common/ContactUs/ContactUs.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface ContactUsProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {}
|
||||
|
||||
export default function FeedbackForm({ className, ...props }: ContactUsProps) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'grid max-w-md grid-flow-row gap-2 py-4 px-5',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Text variant="h3" component="h2">
|
||||
Contact us
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
To report issues with Nhost, please open a GitHub issue in the{' '}
|
||||
<Link
|
||||
href="https://github.com/nhost/nhost/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
nhost/nhost
|
||||
</Link>{' '}
|
||||
repository.
|
||||
</Text>
|
||||
<Text>
|
||||
For issues related to the CLI, please visit the{' '}
|
||||
<Link
|
||||
href="https://github.com/nhost/cli/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
nhost/cli
|
||||
</Link>{' '}
|
||||
repository.
|
||||
</Text>
|
||||
<Text>
|
||||
If you need assistance or have any questions, feel free to join us on{' '}
|
||||
<Link
|
||||
href="https://discord.com/invite/9V7Qb2U"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
Discord
|
||||
</Link>
|
||||
. Alternatively, if you prefer, you can also open a{' '}
|
||||
<Link
|
||||
href="https://github.com/nhost/nhost/discussions/new/choose"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
GitHub discussion
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
<Text>We're here to help, so don't hesitate to reach out!</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
dashboard/src/components/common/ContactUs/index.ts
Normal file
2
dashboard/src/components/common/ContactUs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './ContactUs';
|
||||
export { default as ContactUs } from './ContactUs';
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
|
||||
export default function DepricationNotice() {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
return (
|
||||
!currentProject?.providersUpdated && (
|
||||
<Alert severity="warning" className="grid place-content-center">
|
||||
<Text color="warning" className="max-w-3xl text-sm">
|
||||
On December 1st the old backend domain will cease to work. You need to
|
||||
make sure your client is instantiated using the subdomain and region
|
||||
and update your oauth2 settings. You can find more information{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
href="https://github.com/nhost/nhost/discussions/2303"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</Alert>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ContactUs } from './DepricationNotice';
|
||||
@@ -1,148 +0,0 @@
|
||||
import { Avatar } from '@/components/ui/v2/Avatar';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useInsertFeedbackOneMutation } from '@/utils/__generated__/graphql';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface FeedbackFormProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {}
|
||||
|
||||
// TODO: Use `react-hook-form` here instead of the custom form implementation
|
||||
export default function FeedbackForm({
|
||||
className,
|
||||
...props
|
||||
}: FeedbackFormProps) {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [insertFeedback, { loading }] = useInsertFeedbackOneMutation();
|
||||
const user = useUserData();
|
||||
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [feedbackSent, setFeedbackSent] = useState(false);
|
||||
|
||||
function handleClose() {
|
||||
setTimeout(() => {
|
||||
setFeedbackSent(false);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
const feedbackWithProjectInfo = [
|
||||
currentProject && `Project ID: ${currentProject.id}`,
|
||||
typeof window !== 'undefined' && `URL: ${window.location.href}`,
|
||||
feedback,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
|
||||
try {
|
||||
await insertFeedback({
|
||||
variables: {
|
||||
feedback: {
|
||||
feedback: feedbackWithProjectInfo,
|
||||
},
|
||||
},
|
||||
});
|
||||
setFeedbackSent(true);
|
||||
setFeedback('');
|
||||
} catch (error) {
|
||||
// TODO: Display error to user and use a logging solution
|
||||
}
|
||||
}
|
||||
|
||||
if (feedbackSent) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'grid max-w-md grid-flow-row justify-center gap-4 py-4 px-5 text-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Image
|
||||
src="/assets/FeedbackReceived.svg"
|
||||
alt="Light bulb with a checkmark"
|
||||
width={72}
|
||||
height={72}
|
||||
/>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Text variant="h3" component="h2" className="text-center">
|
||||
Feedback Received
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Thanks for sending us your thoughts! Feel free to send more feedback
|
||||
as you explore the beta, and stay tuned for updates.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="mt-2 text-sm+ font-normal"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'grid max-w-md grid-flow-row gap-2 py-4 px-5',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Text variant="h3" component="h2">
|
||||
Leave Feedback
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Nhost is still in beta and not everything is in place yet, but we'd
|
||||
love to know what you think of it so far.
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-col place-content-between gap-2">
|
||||
<Text className="font-medium">
|
||||
What do you think we should improve?
|
||||
</Text>
|
||||
|
||||
<Avatar
|
||||
className="h-6 w-6 rounded-full"
|
||||
alt={user?.displayName}
|
||||
src={user?.avatarUrl}
|
||||
>
|
||||
{user?.displayName}
|
||||
</Avatar>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
multiline
|
||||
value={feedback}
|
||||
onChange={(event) => setFeedback(event.target.value)}
|
||||
placeholder="Your feedback"
|
||||
rows={6}
|
||||
required
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={!feedback} loading={loading}>
|
||||
Send Feedback
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './FeedbackForm';
|
||||
export { default as FeedbackForm } from './FeedbackForm';
|
||||
@@ -15,6 +15,7 @@ export type PaginationProps = DetailedHTMLProps<
|
||||
* Total number of pages.
|
||||
*/
|
||||
totalNrOfPages: number;
|
||||
|
||||
/**
|
||||
* Number of total elements per page.
|
||||
*/
|
||||
@@ -23,6 +24,10 @@ export type PaginationProps = DetailedHTMLProps<
|
||||
* Total number of elements.
|
||||
*/
|
||||
totalNrOfElements: number;
|
||||
/**
|
||||
* Label of the elements displayed ex: pages, users...
|
||||
*/
|
||||
itemsLabel: string;
|
||||
/**
|
||||
* Current page number.
|
||||
*/
|
||||
@@ -64,6 +69,7 @@ export default function Pagination({
|
||||
elementsPerPage,
|
||||
onPageChange,
|
||||
totalNrOfElements,
|
||||
itemsLabel,
|
||||
...props
|
||||
}: PaginationProps) {
|
||||
return (
|
||||
@@ -132,7 +138,7 @@ export default function Pagination({
|
||||
{totalNrOfElements < currentPageNumber * elementsPerPage
|
||||
? totalNrOfElements
|
||||
: currentPageNumber * elementsPerPage}{' '}
|
||||
of {totalNrOfElements} users
|
||||
of {totalNrOfElements} {itemsLabel}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { NhostIcon } from '@/components/presentational/NhostIcon';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ChangePlanModal } from '@/features/projects/common/components/ChangePlanModal';
|
||||
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface UpgradeToProBannerProps {
|
||||
title: string;
|
||||
description: string | ReactNode;
|
||||
}
|
||||
|
||||
export default function UpgradeToProBanner({
|
||||
title,
|
||||
description,
|
||||
}: UpgradeToProBannerProps) {
|
||||
const { openDialog, openAlertDialog } = useDialog();
|
||||
const isOwner = useIsCurrentUserOwner();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ backgroundColor: 'primary.light' }}
|
||||
className="flex flex-col justify-between space-y-4 rounded-md p-4 lg:flex-row lg:items-center lg:space-y-0"
|
||||
>
|
||||
<div className="flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-2 xs:flex-row xs:space-y-0 xs:space-x-2">
|
||||
<Text>Available with</Text>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<NhostIcon />
|
||||
<Text sx={{ color: 'primary.main' }} className="font-semibold">
|
||||
Nhost Pro
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Text variant="h3">{title}</Text>
|
||||
{typeof description === 'string' ? (
|
||||
<Text>{description}</Text>
|
||||
) : (
|
||||
description
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-2">
|
||||
<Button
|
||||
className="rounded-md"
|
||||
onClick={() => {
|
||||
if (isOwner) {
|
||||
openDialog({
|
||||
component: <ChangePlanModal />,
|
||||
props: {
|
||||
PaperProps: { className: 'p-0 max-w-xl w-full' },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
openAlertDialog({
|
||||
title: "You can't upgrade this project",
|
||||
payload: (
|
||||
<Text variant="subtitle1" component="span">
|
||||
Ask an owner of this workspace to upgrade the project.
|
||||
</Text>
|
||||
),
|
||||
props: {
|
||||
secondaryButtonText: 'I understand',
|
||||
hidePrimaryAction: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
<Link
|
||||
href="https://nhost.io/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
className="text-center font-medium"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
See all features
|
||||
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Image
|
||||
src="/illustration-unbox.png"
|
||||
width={300}
|
||||
height={140}
|
||||
objectFit="contain"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as UpgradeToProBanner } from './UpgradeToProBanner';
|
||||
@@ -178,6 +178,22 @@ export default function DataGridBody<T extends object>({
|
||||
}
|
||||
}
|
||||
|
||||
const getBackgroundCellColor = (
|
||||
row: Row<T>,
|
||||
column: DataBrowserGridColumn<T>,
|
||||
) => {
|
||||
// Grey out files not uploaded
|
||||
if (!row.values.isUploaded) {
|
||||
return 'grey.200';
|
||||
}
|
||||
|
||||
if (column.isDisabled) {
|
||||
return 'grey.100';
|
||||
}
|
||||
|
||||
return 'background.paper';
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
||||
{rows.length === 0 && !loading && (
|
||||
@@ -260,9 +276,7 @@ export default function DataGridBody<T extends object>({
|
||||
})}
|
||||
cell={cell}
|
||||
sx={{
|
||||
backgroundColor: column.isDisabled
|
||||
? 'grey.100'
|
||||
: 'background.paper',
|
||||
backgroundColor: getBackgroundCellColor(row, column),
|
||||
color: isCellDisabled ? 'text.secondary' : 'text.primary',
|
||||
}}
|
||||
className={twMerge(
|
||||
|
||||
46
dashboard/src/components/layout/AILayout/AILayout.tsx
Normal file
46
dashboard/src/components/layout/AILayout/AILayout.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { AISidebar } from '@/components/layout/AISidebar';
|
||||
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import type { SettingsSidebarProps } from '@/components/layout/SettingsSidebar';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface AILayoutProps extends ProjectLayoutProps {
|
||||
/**
|
||||
* Props passed to the sidebar component.
|
||||
*/
|
||||
sidebarProps?: SettingsSidebarProps;
|
||||
}
|
||||
|
||||
export default function AILayout({
|
||||
children,
|
||||
mainContainerProps: {
|
||||
className: mainContainerClassName,
|
||||
...mainContainerProps
|
||||
} = {},
|
||||
sidebarProps: { className: sidebarClassName, ...sidebarProps } = {},
|
||||
...props
|
||||
}: AILayoutProps) {
|
||||
return (
|
||||
<ProjectLayout
|
||||
mainContainerProps={{
|
||||
className: twMerge('flex h-full', mainContainerClassName),
|
||||
...mainContainerProps,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<AISidebar
|
||||
className={twMerge('w-full max-w-sidebar', sidebarClassName)}
|
||||
{...sidebarProps}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="flex w-full flex-auto flex-col overflow-scroll overflow-x-hidden"
|
||||
>
|
||||
<RetryableErrorBoundary>{children}</RetryableErrorBoundary>
|
||||
</Box>
|
||||
</ProjectLayout>
|
||||
);
|
||||
}
|
||||
2
dashboard/src/components/layout/AILayout/index.ts
Normal file
2
dashboard/src/components/layout/AILayout/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './AILayout';
|
||||
export { default as SettingsLayout } from './AILayout';
|
||||
143
dashboard/src/components/layout/AISidebar/AISidebar.tsx
Normal file
143
dashboard/src/components/layout/AISidebar/AISidebar.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { NavLink } from '@/components/common/NavLink';
|
||||
import { Backdrop } from '@/components/ui/v2/Backdrop';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface AISidebarProps extends Omit<BoxProps, 'children'> {}
|
||||
|
||||
interface AINavLinkProps extends ListItemButtonProps {
|
||||
/**
|
||||
* Link to navigate to.
|
||||
*/
|
||||
href: string;
|
||||
/**
|
||||
* Determines whether or not the link should be active if it's href exactly
|
||||
* matches the current route.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
function AINavLink({ exact = true, href, children, ...props }: AINavLinkProps) {
|
||||
const router = useRouter();
|
||||
const baseUrl = `/${router.query.workspaceSlug}/${router.query.appSlug}/ai`;
|
||||
const finalUrl = href && href !== '/' ? `${baseUrl}${href}` : baseUrl;
|
||||
|
||||
const active = exact
|
||||
? router.asPath === finalUrl
|
||||
: router.asPath.startsWith(finalUrl);
|
||||
|
||||
return (
|
||||
<ListItem.Root>
|
||||
<ListItem.Button
|
||||
dense
|
||||
href={finalUrl}
|
||||
component={NavLink}
|
||||
selected={active}
|
||||
{...props}
|
||||
>
|
||||
<ListItem.Text>{children}</ListItem.Text>
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
function toggleExpanded() {
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
|
||||
function handleSelect() {
|
||||
setExpanded(false);
|
||||
}
|
||||
|
||||
function closeSidebarWhenEscapeIsPressed(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
setExpanded(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('keydown', closeSidebarWhenEscapeIsPressed);
|
||||
}
|
||||
|
||||
return () =>
|
||||
document.removeEventListener('keydown', closeSidebarWhenEscapeIsPressed);
|
||||
}, []);
|
||||
|
||||
if (!currentProject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Backdrop
|
||||
open={expanded}
|
||||
className="absolute top-0 left-0 bottom-0 right-0 z-[34] md:hidden"
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
onClick={() => setExpanded(false)}
|
||||
aria-label="Close sidebar overlay"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
|
||||
setExpanded(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
component="aside"
|
||||
className={twMerge(
|
||||
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pt-2 pb-17 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
|
||||
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<nav aria-label="Settings navigation">
|
||||
<List className="grid gap-2">
|
||||
<AINavLink
|
||||
href="/auto-embeddings"
|
||||
exact={false}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
Auto-Embeddings
|
||||
</AINavLink>
|
||||
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
||||
Assistants
|
||||
</AINavLink>
|
||||
</List>
|
||||
</nav>
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
className="absolute bottom-4 left-4 z-[38] h-11 w-11 rounded-full md:hidden"
|
||||
onClick={toggleExpanded}
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<Image
|
||||
width={16}
|
||||
height={16}
|
||||
src="/assets/table.svg"
|
||||
alt="A monochrome table"
|
||||
/>
|
||||
</IconButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
2
dashboard/src/components/layout/AISidebar/index.ts
Normal file
2
dashboard/src/components/layout/AISidebar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './AISidebar';
|
||||
export { default as AISidebar } from './AISidebar';
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FeedbackForm } from '@/components/common/FeedbackForm';
|
||||
import { ContactUs } from '@/components/common/ContactUs';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { NavLink } from '@/components/common/NavLink';
|
||||
import { AccountMenu } from '@/components/layout/AccountMenu';
|
||||
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
|
||||
@@ -6,14 +7,19 @@ import { LocalAccountMenu } from '@/components/layout/LocalAccountMenu';
|
||||
import { MobileNav } from '@/components/layout/MobileNav';
|
||||
import { Logo } from '@/components/presentational/Logo';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Chip } from '@/components/ui/v2/Chip';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
|
||||
import { DevAssistant } from '@/features/ai/DevAssistant';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { DetailedHTMLProps, HTMLProps, PropsWithoutRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface HeaderProps
|
||||
@@ -23,9 +29,14 @@ export interface HeaderProps
|
||||
|
||||
export default function Header({ className, ...props }: HeaderProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { openDrawer } = useDialog();
|
||||
|
||||
const { currentProject, refetch: refetchProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
|
||||
const isProjectUpdating =
|
||||
currentProject?.appStates[0]?.stateId === ApplicationStatus.Updating;
|
||||
|
||||
@@ -44,6 +55,23 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
};
|
||||
}, [isProjectUpdating, refetchProject]);
|
||||
|
||||
const openDevAssistant = () => {
|
||||
// The dev assistant can be only answer questions related to a particular project
|
||||
if (!currentProject) {
|
||||
toast.error('You need to be inside a project to open the Assistant', {
|
||||
style: getToastStyleProps().style,
|
||||
...getToastStyleProps().error,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
openDrawer({
|
||||
title: <GraphiteIcon />,
|
||||
component: <DevAssistant />,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="header"
|
||||
@@ -69,20 +97,24 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
</div>
|
||||
|
||||
<div className="hidden grid-flow-col items-center gap-2 sm:grid">
|
||||
<Button className="rounded-full" onClick={openDevAssistant}>
|
||||
<GraphiteIcon />
|
||||
</Button>
|
||||
|
||||
{isPlatform && (
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
hideChevron
|
||||
className="rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
|
||||
>
|
||||
Feedback
|
||||
Contact us
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
>
|
||||
<FeedbackForm className="max-w-md" />
|
||||
<ContactUs className="max-w-md" />
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeedbackForm } from '@/components/common/FeedbackForm';
|
||||
import { ContactUs } from '@/components/common/ContactUs';
|
||||
import { NavLink } from '@/components/common/NavLink';
|
||||
import { ThemeSwitcher } from '@/components/common/ThemeSwitcher';
|
||||
import { Nav } from '@/components/presentational/Nav';
|
||||
@@ -171,7 +171,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
className="w-full"
|
||||
role={undefined}
|
||||
>
|
||||
<ListItem.Text>Feedback</ListItem.Text>
|
||||
<ListItem.Text>Contact us</ListItem.Text>
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
</Dropdown.Trigger>
|
||||
@@ -180,7 +180,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<FeedbackForm className="max-w-md" />
|
||||
<ContactUs className="max-w-md" />
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
)}
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function SettingsContainer({
|
||||
<Box
|
||||
{...root}
|
||||
className={twMerge(
|
||||
'grid grid-flow-row gap-4 rounded-lg border-1 py-4',
|
||||
'grid grid-flow-row gap-4 overflow-hidden rounded-lg border-1 py-4',
|
||||
root?.className || rootClassName,
|
||||
)}
|
||||
>
|
||||
@@ -128,7 +128,11 @@ export default function SettingsContainer({
|
||||
icon}
|
||||
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text className="text-lg font-semibold">{title}</Text>
|
||||
{typeof title === 'string' ? (
|
||||
<Text className="text-lg font-semibold">{title}</Text>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
|
||||
{description && <Text color="secondary">{description}</Text>}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
|
||||
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import type { SettingsSidebarProps } from '@/components/layout/SettingsSidebar';
|
||||
import { SettingsSidebar } from '@/components/layout/SettingsSidebar';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface SettingsLayoutProps extends ProjectLayoutProps {
|
||||
@@ -22,6 +27,10 @@ export default function SettingsLayout({
|
||||
sidebarProps: { className: sidebarClassName, ...sidebarProps } = {},
|
||||
...props
|
||||
}: SettingsLayoutProps) {
|
||||
const theme = useTheme();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const hasGitRepo = !!currentProject?.githubRepository;
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
mainContainerProps={{
|
||||
@@ -37,9 +46,49 @@ export default function SettingsLayout({
|
||||
|
||||
<Box
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="flex w-full flex-auto flex-col overflow-x-hidden"
|
||||
className="flex w-full flex-auto flex-col overflow-scroll overflow-x-hidden"
|
||||
>
|
||||
<RetryableErrorBoundary>{children}</RetryableErrorBoundary>
|
||||
<RetryableErrorBoundary>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<DepricationNotice />
|
||||
{hasGitRepo && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="grid grid-flow-row place-content-center gap-2"
|
||||
>
|
||||
<Text color="warning" className="text-sm ">
|
||||
As you have a connected repository, make sure to synchronize
|
||||
your changes with{' '}
|
||||
<code
|
||||
className={twMerge(
|
||||
'rounded-md px-2 py-px',
|
||||
theme.palette.mode === 'dark'
|
||||
? 'bg-brown text-copper'
|
||||
: 'bg-slate-200 text-slate-700',
|
||||
)}
|
||||
>
|
||||
nhost config pull
|
||||
</code>{' '}
|
||||
or they may be reverted with the next push.
|
||||
<br />
|
||||
If there are multiple projects linked to the same repository
|
||||
and you only want these changes to apply to a subset of them,
|
||||
please check out{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
href="https://docs.nhost.io/cli/overlays"
|
||||
>
|
||||
docs.nhost.io/cli/overlays
|
||||
</a>{' '}
|
||||
for guidance.
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</RetryableErrorBoundary>
|
||||
</Box>
|
||||
</ProjectLayout>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { List } from '@/components/ui/v2/List';
|
||||
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { isK8SPostgresEnabledInCurrentEnvironment } from '@/utils/helpers';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -135,15 +134,13 @@ export default function SettingsSidebar({
|
||||
>
|
||||
Compute Resources
|
||||
</SettingsNavLink>
|
||||
{isK8SPostgresEnabledInCurrentEnvironment && (
|
||||
<SettingsNavLink
|
||||
href="/database"
|
||||
exact={false}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
Database
|
||||
</SettingsNavLink>
|
||||
)}
|
||||
<SettingsNavLink
|
||||
href="/database"
|
||||
exact={false}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
Database
|
||||
</SettingsNavLink>
|
||||
<SettingsNavLink
|
||||
href="/hasura"
|
||||
exact={false}
|
||||
@@ -203,6 +200,17 @@ export default function SettingsSidebar({
|
||||
>
|
||||
Secrets
|
||||
</SettingsNavLink>
|
||||
|
||||
<SettingsNavLink
|
||||
href="/custom-domains"
|
||||
exact={false}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
Custom Domains
|
||||
</SettingsNavLink>
|
||||
<SettingsNavLink href="/ai" exact={false} onClick={handleSelect}>
|
||||
AI
|
||||
</SettingsNavLink>
|
||||
</List>
|
||||
</nav>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ForwardedRef, SVGProps } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
function NhostIcon(
|
||||
props: SVGProps<SVGSVGElement>,
|
||||
ref: ForwardedRef<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Logo of Nhost"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_9802_20458)">
|
||||
<rect width="24" height="24" fill="#0052CD" />
|
||||
<path
|
||||
d="M17.4656 7.39804L12.4705 4.51369C12.0223 4.25553 11.466 4.25553 11.0169 4.51369C10.5688 4.77276 10.2906 5.25455 10.2906 5.77179V6.14813L9.96517 5.95996C9.51702 5.70179 8.96069 5.70179 8.51163 5.95996C8.06348 6.21903 7.78531 6.70082 7.78531 7.21896V7.5953L7.45988 7.40713C7.01173 7.14897 6.4554 7.14897 6.00634 7.40713C5.55819 7.66621 5.28003 8.14799 5.28003 8.66614V17.7037C5.28003 17.9637 5.43093 18.2055 5.66546 18.3182C5.89908 18.4318 6.1827 18.4009 6.38632 18.24L8.86342 16.2865L12.6832 18.4918C12.7886 18.5527 12.9068 18.5827 13.025 18.5827C13.1431 18.5827 13.2613 18.5518 13.3668 18.4918C13.5777 18.37 13.7086 18.1437 13.7086 17.9001V12.4613C13.7086 11.5687 13.2286 10.7378 12.4559 10.2915L11.2033 9.56789V5.7727C11.2033 5.57998 11.3069 5.4 11.4742 5.30364C11.6414 5.20728 11.8487 5.20728 12.0159 5.30364L17.0111 8.18708C17.5028 8.4707 17.8083 9.00066 17.8083 9.56789V16.3402C17.8083 16.5329 17.7046 16.7129 17.5374 16.8092L16.2138 17.5737V11.0142C16.2138 10.1215 15.7339 9.29064 14.9612 8.84431L11.8859 7.06897V8.12072L14.5058 9.63334C14.9976 9.91696 15.303 10.446 15.303 11.0142V17.9673C15.303 18.21 15.4339 18.4373 15.6448 18.5591C15.7502 18.62 15.8684 18.65 15.9866 18.65C16.1048 18.65 16.2229 18.6191 16.3284 18.5591L17.9937 17.5974C18.4419 17.3383 18.72 16.8565 18.72 16.3383V9.56608C18.7182 8.67614 18.2382 7.84438 17.4656 7.39804ZM11.9987 11.0805C12.4905 11.3641 12.7959 11.8932 12.7959 12.4613V17.5064L9.63246 15.6802L10.6478 14.8803C10.9996 14.603 11.2014 14.1876 11.2014 13.7394V10.6215L11.9987 11.0805ZM10.2906 10.0942V13.7376C10.2906 13.9049 10.2152 14.0603 10.0842 14.163L6.19088 17.2328V8.66523C6.19088 8.47251 6.29451 8.29253 6.46177 8.19617C6.62903 8.09981 6.83629 8.09981 7.00355 8.19617L7.78531 8.64705V15.1057L8.69616 14.3876V7.21896C8.69616 7.02625 8.79979 6.84626 8.96705 6.7499C9.13431 6.65355 9.34157 6.65355 9.50883 6.7499L10.2906 7.20078V9.04157L9.37975 8.51524V9.56789L10.2906 10.0942Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9802_20458">
|
||||
<rect width="24" height="24" rx="4" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(NhostIcon);
|
||||
@@ -0,0 +1 @@
|
||||
export { default as NhostIcon } from './NhostIcon';
|
||||
100
dashboard/src/components/settings/ProvidersUpdatedAlert.tsx
Normal file
100
dashboard/src/components/settings/ProvidersUpdatedAlert.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { useConfirmProvidersUpdatedMutation } from '@/utils/__generated__/graphql';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function ProvidersUpdatedAlert() {
|
||||
const theme = useTheme();
|
||||
const { openAlertDialog } = useDialog();
|
||||
const [confirmed, setConfirmed] = useState(true);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [confirmProvidersUpdated] = useConfirmProvidersUpdatedMutation({
|
||||
variables: { id: currentProject?.id },
|
||||
});
|
||||
|
||||
async function handleSubmitConfirmation() {
|
||||
const confirmProvidersUpdatedPromise = confirmProvidersUpdated();
|
||||
|
||||
await toast.promise(
|
||||
confirmProvidersUpdatedPromise,
|
||||
{
|
||||
loading: 'Confirming...',
|
||||
success: 'Your settings have been updated successfully.',
|
||||
error: 'An error occurred while trying to confirm the message.',
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
setConfirmed(false);
|
||||
}
|
||||
|
||||
function handleOpenConfirmationDialog() {
|
||||
openAlertDialog({
|
||||
title: 'Confirm all providers updated?',
|
||||
payload: (
|
||||
<Text variant="subtitle1" component="span">
|
||||
Please make sure to update all providers before continuing. Your
|
||||
sign-in flows might break if you don't.
|
||||
</Text>
|
||||
),
|
||||
props: {
|
||||
onPrimaryAction: handleSubmitConfirmation,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="grid items-center grid-flow-row gap-2 p-4 place-items-center lg:grid-flow-col lg:place-content-between"
|
||||
>
|
||||
<div className="grid grid-flow-row gap-1 text-left">
|
||||
<Text className="font-semibold">
|
||||
Please update the Redirect URL for all providers being used
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm+">
|
||||
We are deprecating your project's old DNS name in favor of
|
||||
individual DNS names for each service. Please make sure to update your
|
||||
providers to use the new auth specific URL under <b>Redirect URL</b>{' '}
|
||||
before the 1st of February 2023.{' '}
|
||||
<Link
|
||||
href="https://github.com/nhost/nhost/discussions/1319"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
className="font-medium"
|
||||
>
|
||||
Read the discussion here.
|
||||
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
|
||||
</Link>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
className={
|
||||
theme.palette.mode === 'dark'
|
||||
? 'text-white hover:bg-brown'
|
||||
: 'text-black hover:bg-orange-300'
|
||||
}
|
||||
onClick={handleOpenConfirmationDialog}
|
||||
>
|
||||
I have updated all Redirect URLs
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/components/settings/index.ts
Normal file
1
dashboard/src/components/settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ProvidersUpdatedAlert } from './ProvidersUpdatedAlert';
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
export interface AlertProps extends BoxProps {
|
||||
/**
|
||||
@@ -11,19 +11,25 @@ export interface AlertProps extends BoxProps {
|
||||
severity?: 'info' | 'success' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
const StyledBox = styled(Box)(({ theme }) => ({
|
||||
borderRadius: 4,
|
||||
padding: theme.spacing(1.5, 2),
|
||||
textAlign: 'center',
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
lineHeight: theme.typography.pxToRem(22),
|
||||
'@media (prefers-reduced-motion: no-preference)': {
|
||||
transition: theme.transitions.create('background-color'),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function Alert({
|
||||
severity = 'info',
|
||||
children,
|
||||
className,
|
||||
sx,
|
||||
...props
|
||||
}: AlertProps) {
|
||||
return (
|
||||
<Box
|
||||
className={twMerge(
|
||||
'rounded-sm+ bg-opacity-20 p-4 text-center text-sm+ motion-safe:transition-colors',
|
||||
className,
|
||||
)}
|
||||
<StyledBox
|
||||
sx={[
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
severity === 'error' && {
|
||||
@@ -43,6 +49,6 @@ export default function Alert({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</StyledBox>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import MaterialLinearProgress, {
|
||||
|
||||
export interface LinearProgressProps extends MaterialLinearProgressProps {}
|
||||
|
||||
const LinearProgress = styled(MaterialLinearProgress)(({ theme }) => ({
|
||||
const LinearProgress = styled(MaterialLinearProgress)(({ theme, value }) => ({
|
||||
height: 12,
|
||||
borderRadius: 1,
|
||||
[`&.${linearProgressClasses.colorPrimary}`]: {
|
||||
backgroundColor: theme.palette.grey[300],
|
||||
},
|
||||
[`& .${linearProgressClasses.bar}`]: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
backgroundColor:
|
||||
value >= 100 ? theme.palette.error.dark : theme.palette.primary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { textClasses } from '@/components/ui/v2/Text';
|
||||
import { getTypographyUtilityClass, styled } from '@mui/material';
|
||||
import type { ListItemTextProps as MaterialListItemTextProps } from '@mui/material/ListItemText';
|
||||
import MaterialListItemText, {
|
||||
listItemTextClasses,
|
||||
listItemTextClasses as materialListItemTextClasses,
|
||||
} from '@mui/material/ListItemText';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface ListItemTextProps extends MaterialListItemTextProps {}
|
||||
|
||||
const listItemTextClasses = {
|
||||
...materialListItemTextClasses,
|
||||
warning: getTypographyUtilityClass('colorWarning'),
|
||||
};
|
||||
|
||||
const StyledListItemText = styled(MaterialListItemText)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
display: 'grid',
|
||||
@@ -16,6 +23,9 @@ const StyledListItemText = styled(MaterialListItemText)(({ theme }) => ({
|
||||
[`&.${listItemTextClasses.root}`]: {
|
||||
margin: 0,
|
||||
},
|
||||
[`&.${listItemTextClasses.warning}`]: {
|
||||
color: theme.palette.warning.dark,
|
||||
},
|
||||
[`& > .${listItemTextClasses.primary}`]: {
|
||||
fontWeight: 500,
|
||||
textOverflow: 'ellipsis',
|
||||
@@ -29,8 +39,23 @@ const StyledListItemText = styled(MaterialListItemText)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
function ListItemText({ children, ...props }: ListItemTextProps) {
|
||||
return <StyledListItemText {...props}>{children}</StyledListItemText>;
|
||||
function ListItemText({
|
||||
children,
|
||||
color = 'primary',
|
||||
className,
|
||||
...props
|
||||
}: ListItemTextProps) {
|
||||
return (
|
||||
<StyledListItemText
|
||||
className={clsx(
|
||||
color === 'warning' && textClasses.colorWarning,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</StyledListItemText>
|
||||
);
|
||||
}
|
||||
|
||||
ListItemText.displayName = 'NhostListItemText';
|
||||
|
||||
@@ -18,7 +18,7 @@ export type TextProps<
|
||||
*
|
||||
* @default 'primary'
|
||||
*/
|
||||
color?: 'primary' | 'secondary' | 'disabled' | 'error';
|
||||
color?: 'primary' | 'secondary' | 'disabled' | 'error' | 'warning';
|
||||
/**
|
||||
* The component used for the root node.
|
||||
*/
|
||||
@@ -31,6 +31,7 @@ const textClasses = {
|
||||
colorSecondary: getTypographyUtilityClass('colorSecondary'),
|
||||
colorDisabled: getTypographyUtilityClass('colorDisabled'),
|
||||
colorError: getTypographyUtilityClass('colorError'),
|
||||
colorWarning: getTypographyUtilityClass('colorWarning'),
|
||||
};
|
||||
|
||||
const StyledTypography = styled(MaterialTypography)<TextProps>(({ theme }) => ({
|
||||
@@ -50,6 +51,9 @@ const StyledTypography = styled(MaterialTypography)<TextProps>(({ theme }) => ({
|
||||
[`&.${textClasses.colorError}`]: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
[`&.${textClasses.colorWarning}`]: {
|
||||
color: theme.palette.warning.dark,
|
||||
},
|
||||
}));
|
||||
|
||||
function Text<
|
||||
@@ -70,6 +74,7 @@ function Text<
|
||||
color === 'secondary' && textClasses.colorSecondary,
|
||||
color === 'disabled' && textClasses.colorDisabled,
|
||||
color === 'error' && textClasses.colorError,
|
||||
color === 'warning' && textClasses.colorWarning,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
24
dashboard/src/components/ui/v2/icons/AIIcon/AIIcon.tsx
Normal file
24
dashboard/src/components/ui/v2/icons/AIIcon/AIIcon.tsx
Normal file
File diff suppressed because one or more lines are too long
1
dashboard/src/components/ui/v2/icons/AIIcon/index.ts
Normal file
1
dashboard/src/components/ui/v2/icons/AIIcon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AIIcon } from './AIIcon';
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function ArrowElbowRightUp(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8 6L11 3L14 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2 12H11V3"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
ArrowElbowRightUp.displayName = 'NhostArrowElbowRightUp';
|
||||
|
||||
export default ArrowElbowRightUp;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ArrowElbowRightUp } from './ArrowElbowRightUp';
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
|
||||
function ArrowsClockwise(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
aria-label="Update"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.0103 6.23227H14.0103V3.23227"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.11084 4.11091C4.62156 3.60019 5.22788 3.19506 5.89517 2.91866C6.56246 2.64226 7.27766 2.5 7.99993 2.5C8.7222 2.5 9.4374 2.64226 10.1047 2.91866C10.772 3.19506 11.3783 3.60019 11.889 4.11091L14.0103 6.23223"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.98975 9.76773H1.98975V12.7677"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11.8892 11.8891C11.3785 12.3998 10.7722 12.8049 10.1049 13.0813C9.43762 13.3577 8.72242 13.5 8.00015 13.5C7.27788 13.5 6.56269 13.3577 5.89539 13.0813C5.2281 12.8049 4.62179 12.3998 4.11107 11.8891L1.98975 9.76776"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
ArrowsClockwise.displayName = 'NhostArrowsClockwise';
|
||||
|
||||
export default ArrowsClockwise;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ArrowsClockwise } from './ArrowsClockwise';
|
||||
40
dashboard/src/components/ui/v2/icons/CubeIcon/CubeIcon.tsx
Normal file
40
dashboard/src/components/ui/v2/icons/CubeIcon/CubeIcon.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
|
||||
function CubeIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M14 11.0826V4.91742C14 4.8287 13.9764 4.74158 13.9316 4.665C13.8868 4.58841 13.8225 4.52513 13.7451 4.48163L8.24513 1.38788C8.17029 1.34578 8.08587 1.32367 8 1.32367C7.91413 1.32367 7.82971 1.34578 7.75487 1.38788L2.25487 4.48163C2.17754 4.52513 2.11318 4.58841 2.0684 4.665C2.02361 4.74158 2 4.8287 2 4.91742V11.0826C2 11.1713 2.02361 11.2584 2.0684 11.335C2.11318 11.4116 2.17754 11.4749 2.25487 11.5184L7.75487 14.6121C7.82971 14.6542 7.91413 14.6763 8 14.6763C8.08587 14.6763 8.17029 14.6542 8.24513 14.6121L13.7451 11.5184C13.8225 11.4749 13.8868 11.4116 13.9316 11.335C13.9764 11.2584 14 11.1713 14 11.0826Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.9311 4.66414L8.0594 8.00001L2.06934 4.66357"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8.05916 8L8.00049 14.6763"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
CubeIcon.displayName = 'NhostCubeIcon';
|
||||
|
||||
export default CubeIcon;
|
||||
1
dashboard/src/components/ui/v2/icons/CubeIcon/index.ts
Normal file
1
dashboard/src/components/ui/v2/icons/CubeIcon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CubeIcon } from './CubeIcon';
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function EmbeddingsIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="17"
|
||||
height="17"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 17 17"
|
||||
fill="none"
|
||||
aria-label="Embeddings Icon"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.178057 4.04687L4.04687 0.178057C4.28428 -0.0593522 4.6692 -0.0593522 4.90661 0.178057L8.77542 4.04687C9.01283 4.28428 9.01283 4.6692 8.77542 4.90661C8.53801 5.14402 8.15309 5.14402 7.91568 4.90661L5.08466 2.07559L5.08466 12.7664H3.86881L3.86881 2.07559L1.03779 4.90661C0.800384 5.14402 0.415467 5.14402 0.178057 4.90661C-0.0593524 4.6692 -0.0593524 4.28428 0.178057 4.04687Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.9531 8.22458L16.8219 12.0934C17.0594 12.3308 17.0594 12.7157 16.8219 12.9531L12.9531 16.8219C12.7157 17.0594 12.3308 17.0594 12.0934 16.8219C11.856 16.5845 11.856 16.1996 12.0934 15.9622L14.9244 13.1312H4.23357V11.9153H14.9244L12.0934 9.08432C11.856 8.84691 11.856 8.46199 12.0934 8.22458C12.3308 7.98717 12.7157 7.98717 12.9531 8.22458Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
EmbeddingsIcon.displayName = 'NhostEmbeddingsIcon';
|
||||
|
||||
export default EmbeddingsIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as EmbeddingsIcon } from './EmbeddingsIcons';
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function GraphiteIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="22"
|
||||
height="25"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 25"
|
||||
aria-label="Graphite"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.39873 13.0137C12.2825 13.0137 14.6203 10.7138 14.6203 7.87671C14.6203 5.03963 12.2825 2.73973 9.39873 2.73973C6.51497 2.73973 4.17722 5.03963 4.17722 7.87671C4.17722 10.7138 6.51497 13.0137 9.39873 13.0137ZM9.39873 15.7534C13.8205 15.7534 17.4051 12.2269 17.4051 7.87671C17.4051 3.52652 13.8205 0 9.39873 0C4.97696 0 1.39241 3.52652 1.39241 7.87671C1.39241 12.2269 4.97696 15.7534 9.39873 15.7534Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.78481 15.7534C2.78481 19.3471 5.74597 22.2603 9.39873 22.2603C13.0515 22.2603 16.0127 19.3471 16.0127 15.7534H18.7975C18.7975 20.8602 14.5895 25 9.39873 25C4.20796 25 0 20.8602 0 15.7534H2.78481Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.37975 1.36986C7.37975 0.613309 8.00315 0 8.77215 0H20.6076C21.3766 0 22 0.613309 22 1.36986C22 2.12642 21.3766 2.73973 20.6076 2.73973H8.77215C8.00315 2.73973 7.37975 2.12642 7.37975 1.36986Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
GraphiteIcon.displayName = 'NhostGraphiteIcon';
|
||||
|
||||
export default GraphiteIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as GraphiteIcon } from './GraphiteIcon';
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function ServicesIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="Services"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.89295 4.15125H9.21701C9.28097 4.15125 9.33291 4.09959 9.33326 4.03565V2.8556C9.33291 2.79163 9.28097 2.73999 9.21701 2.73999H7.89295C7.82909 2.73999 7.77734 2.79174 7.77734 2.8556V4.03562C7.77734 4.09948 7.82911 4.15125 7.89295 4.15125ZM5.53406 5.84862H4.21001C4.14594 5.84826 4.09411 5.79643 4.09375 5.73236V4.55298C4.09411 4.48902 4.14606 4.43738 4.21001 4.43738H5.53406C5.5979 4.43738 5.64967 4.48912 5.64967 4.55298V5.73236C5.64967 5.79631 5.59801 5.84826 5.53406 5.84862ZM14.6307 6.48419C15.4316 6.48419 15.8114 6.77094 15.8521 6.80325L16 6.92016L15.9386 7.09971C15.8408 7.34738 15.69 7.57067 15.4968 7.75398C15.2062 8.04139 14.6791 8.38436 13.8221 8.38436H13.6839C13.337 9.26145 12.8707 10.2484 12.0879 11.1345C11.6196 11.6644 11.0689 12.1152 10.457 12.4696C9.71438 12.8901 8.90665 13.1835 8.06725 13.3376C7.4634 13.45 6.85036 13.5056 6.23616 13.5036C4.87658 13.5036 3.67717 13.2453 2.93893 12.7932C2.28012 12.3908 1.77374 11.7333 1.43337 10.8407C1.13576 10.0277 0.989105 9.1673 1.00063 8.30169C1.00204 8.04363 1.21146 7.83507 1.46954 7.83472H11.3503C11.471 7.8302 12.0678 7.77917 12.4399 7.57185C12.1318 7.08484 12.0446 6.51519 12.188 5.9087C12.2639 5.59123 12.3932 5.28898 12.5703 5.01479L12.7118 4.81068L12.9268 4.93471L12.9269 4.93473C12.9668 4.9583 13.8447 5.47632 13.9996 6.53843C14.2082 6.50325 14.4192 6.48511 14.6307 6.48419ZM3.7092 7.54529H2.38514C2.32128 7.54529 2.26953 7.49353 2.26953 7.42967V6.25029V6.24964C2.26953 6.1858 2.32128 6.13403 2.38514 6.13403H3.7092H3.70985C3.77369 6.13439 3.82516 6.18643 3.8248 6.25029V7.42969C3.8248 7.49353 3.77306 7.54529 3.7092 7.54529ZM4.21003 7.54529H5.53409C5.59794 7.54529 5.64969 7.49353 5.64969 7.42969V6.25029C5.65005 6.18643 5.59858 6.13439 5.53472 6.13403H5.53407H4.21001C4.14579 6.13403 4.09375 6.18607 4.09375 6.25029V7.42967C4.09413 7.49363 4.14606 7.54529 4.21003 7.54529ZM7.38597 7.54529H6.06191C5.99808 7.54529 5.94631 7.49353 5.94629 7.42967V6.25029V6.24964C5.94629 6.1858 5.99803 6.13403 6.06189 6.13403H7.38595H7.3866C7.45046 6.13439 7.50193 6.18643 7.50157 6.25029V7.42969C7.50157 7.49353 7.44983 7.54529 7.38597 7.54529ZM7.89295 7.54529H9.21701C9.28097 7.54529 9.33291 7.49365 9.33326 7.42969V6.25029C9.33326 6.18607 9.28122 6.13403 9.21701 6.13403H7.89295C7.82909 6.13403 7.77734 6.1858 7.77734 6.24964V6.25029V7.42967C7.77734 7.49353 7.82911 7.54529 7.89295 7.54529ZM6.06189 5.84862H7.38595C7.4499 5.84826 7.50156 5.79631 7.50156 5.73236V4.55298C7.50156 4.48912 7.44979 4.43738 7.38595 4.43738H6.06189C5.99804 4.43738 5.94629 4.48915 5.94629 4.55298V5.73236C5.94629 5.79631 5.99795 5.84826 6.06189 5.84862ZM9.21701 5.84862H7.89295C7.82901 5.84826 7.77734 5.79631 7.77734 5.73236V4.55298C7.77734 4.48915 7.82909 4.43738 7.89295 4.43738H9.21701C9.28097 4.43738 9.33291 4.48902 9.33326 4.55298V5.73236C9.33291 5.79643 9.28108 5.84826 9.21701 5.84862ZM11.0637 7.54529H9.73963C9.67579 7.54529 9.62402 7.49353 9.62402 7.42967V6.25029V6.24964C9.62402 6.1858 9.67579 6.13403 9.73963 6.13403H11.0637C11.1279 6.13403 11.1799 6.18607 11.1799 6.25029V7.42969C11.1796 7.49365 11.1277 7.54529 11.0637 7.54529Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
ServicesIcon.displayName = 'NhostServicesIcon';
|
||||
|
||||
export default ServicesIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ServicesIcon } from './ServicesIcon';
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function TerminalIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="16"
|
||||
height="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
aria-label="Trash"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.49851 3.43968L2.93795 2.94141L1.94141 4.06252L2.50196 4.56079L6.37134 8.00024L2.50196 11.4397L1.94141 11.938L2.93795 13.0591L3.49851 12.5608L7.99851 8.56079C8.15863 8.41847 8.25024 8.21446 8.25024 8.00024C8.25024 7.78601 8.15863 7.582 7.99851 7.43968L3.49851 3.43968ZM7.99987 11.2502H7.24987V12.7502H7.99987H13.9999H14.7499V11.2502H13.9999H7.99987Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
TerminalIcon.displayName = 'NhostTerminalIcon';
|
||||
|
||||
export default TerminalIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as TerminalIcon } from './TerminalIcon';
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
function WarningIcon(props: IconProps, ref: ForwardedRef<SVGSVGElement>) {
|
||||
return (
|
||||
<SvgIcon
|
||||
width="16"
|
||||
height="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
aria-label="Warning"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8 5.5V9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.135 2.49904L1.63648 11.9986C1.5485 12.1506 1.5021 12.3231 1.50195 12.4987C1.50181 12.6743 1.54792 12.8469 1.63565 12.999C1.72338 13.1512 1.84964 13.2776 2.00172 13.3654C2.15379 13.4533 2.32633 13.4995 2.50196 13.4995H13.499C13.6746 13.4995 13.8472 13.4533 13.9992 13.3654C14.1513 13.2776 14.2776 13.1512 14.3653 12.999C14.453 12.8469 14.4991 12.6743 14.499 12.4987C14.4988 12.3231 14.4524 12.1506 14.3645 11.9986L8.86594 2.49904C8.7781 2.34728 8.6519 2.22129 8.49999 2.1337C8.34809 2.04611 8.17582 2 8.00047 2C7.82512 2 7.65285 2.04611 7.50095 2.1337C7.34904 2.22129 7.22284 2.34728 7.135 2.49904V2.49904Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 12C8.41421 12 8.75 11.6642 8.75 11.25C8.75 10.8358 8.41421 10.5 8 10.5C7.58579 10.5 7.25 10.8358 7.25 11.25C7.25 11.6642 7.58579 12 8 12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
WarningIcon.displayName = 'NhostWarningIcon';
|
||||
|
||||
export default forwardRef(WarningIcon);
|
||||
@@ -0,0 +1 @@
|
||||
export { default as WarningIcon } from './WarningIcon';
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"k8s-postgres": {
|
||||
"enabled": ["dev", "staging", "production"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import {
|
||||
useDeleteUserAccountMutation,
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { type ApolloError } from '@apollo/client';
|
||||
import { useSignOut, useUserData } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
function ConfirmDeleteAccountModal({
|
||||
close,
|
||||
onDelete,
|
||||
}: {
|
||||
onDelete?: () => Promise<any>;
|
||||
close: () => void;
|
||||
}) {
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
|
||||
const user = useUserData();
|
||||
|
||||
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const userHasProjects =
|
||||
!loading && data?.workspaces.some((workspace) => workspace.projects.length);
|
||||
|
||||
const userData = useUserData();
|
||||
|
||||
const [deleteUserAccount] = useDeleteUserAccountMutation({
|
||||
variables: { id: userData?.id },
|
||||
});
|
||||
|
||||
const onClickConfirm = async () => {
|
||||
setLoadingRemove(true);
|
||||
|
||||
await toast.promise(
|
||||
deleteUserAccount(),
|
||||
{
|
||||
loading: 'Deleting your account...',
|
||||
success: `The account has been deleted successfully.`,
|
||||
error: (arg: ApolloError) => {
|
||||
// we need to get the internal error message from the GraphQL error
|
||||
const { internal } = arg.graphQLErrors[0]?.extensions || {};
|
||||
const { message } = (internal as Record<string, any>)?.error || {};
|
||||
|
||||
// we use the default Apollo error message if we can't find the
|
||||
// internal error message
|
||||
return (
|
||||
message ||
|
||||
arg.message ||
|
||||
'An error occurred while deleting your account. Please try again.'
|
||||
);
|
||||
},
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
onDelete?.();
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
Delete Account?
|
||||
</Text>
|
||||
|
||||
{userHasProjects && (
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="font-bold"
|
||||
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
|
||||
>
|
||||
You still have active projects. Please delete your projects before
|
||||
proceeding with the account deletion.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box className="my-4">
|
||||
<Checkbox
|
||||
id="accept-1"
|
||||
label={`I'm sure I want to delete my account`}
|
||||
className="py-2"
|
||||
checked={remove}
|
||||
onChange={(_event, checked) => setRemove(checked)}
|
||||
aria-label="Confirm Delete Project #1"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
color="error"
|
||||
onClick={onClickConfirm}
|
||||
disabled={userHasProjects}
|
||||
loading={loadingRemove}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DeleteAccount() {
|
||||
const router = useRouter();
|
||||
const { signOut } = useSignOut();
|
||||
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
const onDelete = async () => {
|
||||
await signOut();
|
||||
await router.push('/signin');
|
||||
};
|
||||
|
||||
const confirmDeleteAccount = async () => {
|
||||
openDialog({
|
||||
component: (
|
||||
<ConfirmDeleteAccountModal close={closeDialog} onDelete={onDelete} />
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer
|
||||
title="Delete Account"
|
||||
description="Please proceed with caution as the removal of your Personal Account and its contents from the Nhost platform is irreversible. This action will permanently delete your account and all associated data."
|
||||
className="px-0"
|
||||
slotProps={{
|
||||
submitButton: { className: 'hidden' },
|
||||
footer: { className: 'hidden' },
|
||||
}}
|
||||
>
|
||||
<Box className="grid grid-flow-row border-t-1">
|
||||
<Button
|
||||
color="error"
|
||||
className="mx-4 mt-4 justify-self-end"
|
||||
onClick={confirmDeleteAccount}
|
||||
>
|
||||
Delete Personal Account
|
||||
</Button>
|
||||
</Box>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DeleteAccount } from './DeleteAccount';
|
||||
@@ -9,9 +9,11 @@ import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { DotsVerticalIcon } from '@/components/ui/v2/icons/DotsVerticalIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { WarningIcon } from '@/components/ui/v2/icons/WarningIcon';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { CreatePATForm } from '@/features/account/settings/components/CreatePATForm';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
@@ -133,69 +135,91 @@ export default function PATSettings() {
|
||||
<Box className="grid grid-flow-row gap-2">
|
||||
{availablePersonalAccessTokens.length > 0 && (
|
||||
<List>
|
||||
{availablePersonalAccessTokens.map((pat, index) => (
|
||||
<Fragment key={pat.id}>
|
||||
<ListItem.Root
|
||||
className="grid grid-cols-3 gap-2 px-4 pr-12"
|
||||
secondaryAction={
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
disabled={maintenanceActive}
|
||||
aria-label={`More options for ${pat.name}`}
|
||||
{availablePersonalAccessTokens.map((pat, index) => {
|
||||
const tokenHasExpired = new Date(pat.expiresAt) < new Date();
|
||||
|
||||
return (
|
||||
<Fragment key={pat.id}>
|
||||
<ListItem.Root
|
||||
className="grid grid-cols-3 gap-2 px-4 pr-12"
|
||||
secondaryAction={
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<DotsVerticalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
disabled={maintenanceActive}
|
||||
aria-label={`More options for ${pat.name}`}
|
||||
>
|
||||
<DotsVerticalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-32' }}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Dropdown.Item onClick={() => handleConfirmDelete(pat)}>
|
||||
<Text className="font-medium" color="error">
|
||||
Delete
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Text className="truncate">{pat.name}</ListItem.Text>
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-32' }}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleConfirmDelete(pat)}
|
||||
>
|
||||
<Text className="font-medium" color="error">
|
||||
Delete
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Text
|
||||
className="truncate"
|
||||
color={tokenHasExpired ? 'warning' : 'primary'}
|
||||
>
|
||||
<span className="mr-2">{pat.name}</span>
|
||||
{tokenHasExpired && (
|
||||
<Tooltip title="This personal access token is expired.">
|
||||
<WarningIcon className="h-4 w-4" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</ListItem.Text>
|
||||
|
||||
<Text className="truncate">
|
||||
{new Date(pat.expiresAt).toLocaleDateString()}
|
||||
</Text>
|
||||
<Text
|
||||
className="truncate"
|
||||
color={tokenHasExpired ? 'warning' : 'primary'}
|
||||
>
|
||||
{new Date(pat.expiresAt).toLocaleDateString()}
|
||||
</Text>
|
||||
|
||||
<Text className="truncate">
|
||||
{new Date(pat.createdAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</ListItem.Root>
|
||||
<Text
|
||||
className="truncate"
|
||||
color={tokenHasExpired ? 'warning' : 'primary'}
|
||||
>
|
||||
{new Date(pat.createdAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</ListItem.Root>
|
||||
|
||||
<Divider
|
||||
component="li"
|
||||
className={twMerge(
|
||||
index === availablePersonalAccessTokens.length - 1
|
||||
? '!mt-4'
|
||||
: '!my-4',
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
<Divider
|
||||
component="li"
|
||||
className={twMerge(
|
||||
index === availablePersonalAccessTokens.length - 1
|
||||
? '!mt-4'
|
||||
: '!my-4',
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation deleteUserAccount($id: uuid!) {
|
||||
deleteUser(id: $id) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
345
dashboard/src/features/ai/AssistantForm/AssistantForm.tsx
Normal file
345
dashboard/src/features/ai/AssistantForm/AssistantForm.tsx
Normal file
@@ -0,0 +1,345 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowsClockwise } from '@/components/ui/v2/icons/ArrowsClockwise';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { GraphqlDataSourcesFormSection } from '@/features/ai/AssistantForm/components/GraphqlDataSourcesFormSection';
|
||||
import { WebhooksDataSourcesFormSection } from '@/features/ai/AssistantForm/components/WebhooksDataSourcesFormSection';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import {
|
||||
useInsertAssistantMutation,
|
||||
useUpdateAssistantMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import {
|
||||
ApolloClient,
|
||||
HttpLink,
|
||||
InMemoryCache,
|
||||
type ApolloError,
|
||||
} from '@apollo/client';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
name: Yup.string().required('The name is required.'),
|
||||
description: Yup.string(),
|
||||
instructions: Yup.string().required('The instructions are required'),
|
||||
model: Yup.string().required('The model is required'),
|
||||
graphql: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
description: Yup.string().required(),
|
||||
query: Yup.string().required(),
|
||||
arguments: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
description: Yup.string().required(),
|
||||
type: Yup.string().required(),
|
||||
required: Yup.bool().required(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
webhooks: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
description: Yup.string().required(),
|
||||
URL: Yup.string().required(),
|
||||
arguments: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
description: Yup.string().required(),
|
||||
type: Yup.string().required(),
|
||||
required: Yup.bool().required(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export interface AssistantFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
|
||||
*/
|
||||
assistantId?: string;
|
||||
|
||||
/**
|
||||
* if there is initialData then it's an update operation
|
||||
*/
|
||||
initialData?: AssistantFormValues;
|
||||
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
*/
|
||||
onCancel?: VoidFunction;
|
||||
/**
|
||||
* Function to be called when the submit is successful.
|
||||
*/
|
||||
onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
|
||||
}
|
||||
|
||||
export default function AssistantForm({
|
||||
assistantId,
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
location,
|
||||
}: AssistantFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
'graphql',
|
||||
);
|
||||
|
||||
const client = new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: new HttpLink({
|
||||
uri: serviceUrl,
|
||||
headers: {
|
||||
'x-hasura-admin-secret':
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: currentProject?.config?.hasura.adminSecret,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [insertAssistantMutation] = useInsertAssistantMutation({
|
||||
client,
|
||||
});
|
||||
|
||||
const [updateAssistantMutation] = useUpdateAssistantMutation({ client });
|
||||
|
||||
const form = useForm<AssistantFormValues>({
|
||||
defaultValues: initialData,
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting, dirtyFields },
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateAutoEmbeddings = async (
|
||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
||||
) => {
|
||||
// remove any __typename from the form values
|
||||
const payload = removeTypename(values);
|
||||
|
||||
if (values.webhooks.length === 0) {
|
||||
delete payload.webhooks;
|
||||
}
|
||||
|
||||
if (values.graphql.length === 0) {
|
||||
delete payload.graphql;
|
||||
}
|
||||
|
||||
// remove assistantId because the update mutation fails otherwise
|
||||
delete payload.assistantID;
|
||||
|
||||
// If the assistantId is set then we do an update
|
||||
if (assistantId) {
|
||||
await updateAssistantMutation({
|
||||
variables: {
|
||||
id: assistantId,
|
||||
data: payload,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await insertAssistantMutation({
|
||||
variables: {
|
||||
data: {
|
||||
...values,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (
|
||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
||||
) => {
|
||||
try {
|
||||
await toast.promise(
|
||||
createOrUpdateAutoEmbeddings(values),
|
||||
{
|
||||
loading: 'Configuring the Assistant...',
|
||||
success: `The Assistant has been configured successfully.`,
|
||||
error: (arg: ApolloError) => {
|
||||
// we need to get the internal error message from the GraphQL error
|
||||
const { internal } = arg.graphQLErrors[0]?.extensions || {};
|
||||
const { message } = (internal as Record<string, any>)?.error || {};
|
||||
|
||||
// we use the default Apollo error message if we can't find the
|
||||
// internal error message
|
||||
return (
|
||||
message ||
|
||||
arg.message ||
|
||||
'An error occurred while configuring the Assistant. Please try again.'
|
||||
);
|
||||
},
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
onSubmit?.();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden border-t"
|
||||
>
|
||||
<div className="flex flex-1 flex-col space-y-4 overflow-auto p-4">
|
||||
<Input
|
||||
{...register('name')}
|
||||
id="name"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Name</Text>
|
||||
<Tooltip title="Name of the assistant">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.name}
|
||||
helperText={errors?.name?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('description')}
|
||||
id="description"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Description</Text>
|
||||
<Tooltip title={<span>Description of the assistant</span>}>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.description}
|
||||
helperText={errors?.description?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[22px]',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('instructions')}
|
||||
id="instructions"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Instructions</Text>
|
||||
<Tooltip title="Instructions for the assistant. This is used to instruct the AI assistant on how to behave and respond to the user">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.instructions}
|
||||
helperText={errors?.instructions?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[22px]',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('model')}
|
||||
id="model"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Model</Text>
|
||||
<Tooltip title="Model to use for the assistant.">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.model}
|
||||
helperText={errors?.model?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
<GraphqlDataSourcesFormSection />
|
||||
<WebhooksDataSourcesFormSection />
|
||||
</div>
|
||||
|
||||
<Box className="flex w-full flex-row justify-between rounded border-t p-4">
|
||||
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
startIcon={assistantId ? <ArrowsClockwise /> : <PlusIcon />}
|
||||
>
|
||||
{assistantId ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { ControlledSelect } from '@/components/form/ControlledSelect';
|
||||
import { ControlledSwitch } from '@/components/form/ControlledSwitch';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { type AssistantFormValues } from '@/features/ai/AssistantForm/AssistantForm';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
interface ArgumentsFormSectionProps {
|
||||
nestedField: string;
|
||||
nestIndex: number;
|
||||
}
|
||||
|
||||
export default function ArgumentsFormSection({
|
||||
nestedField,
|
||||
nestIndex,
|
||||
}: ArgumentsFormSectionProps) {
|
||||
const form = useFormContext<AssistantFormValues>();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: `${nestedField}.${nestIndex}.arguments`,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className="space-y-4">
|
||||
<div className="flex flex-row items-center justify-between ">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Arguments
|
||||
</Text>
|
||||
<Tooltip title={<span>Arguments</span>}>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button variant="borderless" onClick={() => append({})}>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<Box
|
||||
key={field.id}
|
||||
className="flex flex-col space-y-20 rounded border-1 p-4"
|
||||
sx={{ backgroundColor: 'grey.200' }}
|
||||
>
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<Input
|
||||
// We're putting ts-ignore here so we could use the same components for both graphql and webhooks
|
||||
// by passing the nestedField = 'graphql' or nestedField = 'webhooks'
|
||||
{...register(
|
||||
// @ts-ignore
|
||||
`${nestedField}.${nestIndex}.arguments.${index}.name`,
|
||||
)}
|
||||
id={`${field.id}-name`}
|
||||
placeholder="Name"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={
|
||||
!!errors?.[nestedField]?.[nestIndex]?.arguments[index].name
|
||||
}
|
||||
helperText={
|
||||
errors?.[nestedField]?.[nestIndex]?.arguments[index]?.name
|
||||
?.message
|
||||
}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(
|
||||
// @ts-ignore
|
||||
`${nestedField}.${nestIndex}.arguments.${index}.description`,
|
||||
)}
|
||||
id={`${field.id}-description`}
|
||||
placeholder="Description"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={
|
||||
!!errors?.[nestedField]?.[nestIndex]?.arguments[index]
|
||||
.description
|
||||
}
|
||||
helperText={
|
||||
errors?.[nestedField]?.[nestIndex]?.arguments[index]
|
||||
?.description?.message
|
||||
}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[22px]',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Box className="w-full">
|
||||
<ControlledSelect
|
||||
fullWidth
|
||||
{...register(
|
||||
// @ts-ignore
|
||||
`${nestedField}.${nestIndex}.arguments.${index}.type`,
|
||||
)}
|
||||
id={`${field.id}-type`}
|
||||
placeholder="Select argument type"
|
||||
slotProps={{
|
||||
listbox: { className: 'min-w-0 w-full' },
|
||||
popper: {
|
||||
disablePortal: false,
|
||||
className: 'z-[10000] w-[270px] w-full',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{[
|
||||
'string',
|
||||
'number',
|
||||
'integer',
|
||||
'object',
|
||||
'array',
|
||||
'boolean',
|
||||
]?.map((argumentType) => (
|
||||
<Option key={argumentType} value={argumentType}>
|
||||
{argumentType}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
</Box>
|
||||
<ControlledSwitch
|
||||
{...register(
|
||||
// @ts-ignore
|
||||
`${nestedField}.${nestIndex}.arguments.${index}.required`,
|
||||
)}
|
||||
disabled={false}
|
||||
label={
|
||||
<Text variant="subtitle1" component="span">
|
||||
Required
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="h-10 self-end"
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ArgumentsFormSection } from './ArgumentsFormSection';
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { type AssistantFormValues } from '@/features/ai/AssistantForm/AssistantForm';
|
||||
import { ArgumentsFormSection } from '@/features/ai/AssistantForm/components/ArgumentsFormSection';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
export default function GraphqlDataSourcesFormSection() {
|
||||
const form = useFormContext<AssistantFormValues>();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'graphql',
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className="space-y-4 rounded border-1">
|
||||
<Box className="flex flex-row items-center justify-between p-4 pb-0">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
GraphQL
|
||||
</Text>
|
||||
<Tooltip title="GraphQL data sources and tools. Run against the project's GraphQL API">
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() =>
|
||||
append({
|
||||
name: '',
|
||||
description: '',
|
||||
query: '',
|
||||
arguments: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box className="flex flex-col space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<Box key={field.id} className="flex flex-col space-y-4">
|
||||
<Box className="flex w-full flex-col space-y-4 p-4 pt-0">
|
||||
<Input
|
||||
{...register(`graphql.${index}.name`)}
|
||||
id={`${field.id}-name`}
|
||||
label="Name"
|
||||
placeholder="Name"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.graphql?.at(index)?.name}
|
||||
helperText={errors?.graphql?.at(index)?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`graphql.${index}.description`)}
|
||||
id={`${field.id}-description`}
|
||||
label="Description"
|
||||
placeholder="Description"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.graphql?.at(index)?.description}
|
||||
helperText={errors?.graphql?.at(index)?.description?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[22px]',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`graphql.${index}.query`)}
|
||||
id={`${field.id}-query`}
|
||||
label="Query"
|
||||
placeholder="Query"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.graphql?.at(index)?.query}
|
||||
helperText={errors?.graphql?.at(index)?.query?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[22px]',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ArgumentsFormSection nestedField="graphql" nestIndex={index} />
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="h-10 self-end"
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{index < fields.length - 1 && (
|
||||
<Divider className="h-px" sx={{ background: 'grey.200' }} />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as GraphqlDataSourcesFormSection } from './GraphqlDataSourcesFormSection';
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { type AssistantFormValues } from '@/features/ai/AssistantForm/AssistantForm';
|
||||
import { ArgumentsFormSection } from '@/features/ai/AssistantForm/components/ArgumentsFormSection';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
export default function WebhooksDataSourcesFormSection() {
|
||||
const form = useFormContext<AssistantFormValues>();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'webhooks',
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className="space-y-4 rounded border-1">
|
||||
<Box className="flex flex-row items-center justify-between p-4">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Webhooks
|
||||
</Text>
|
||||
<Tooltip title="Webhook data sources and tools">
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() =>
|
||||
append({
|
||||
name: '',
|
||||
description: '',
|
||||
URL: '',
|
||||
arguments: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box className="flex flex-col space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<Box key={field.id} className="flex flex-col space-y-4">
|
||||
<Box className="flex w-full flex-col space-y-4 p-4 pt-0">
|
||||
<Input
|
||||
{...register(`webhooks.${index}.name`)}
|
||||
id={`${field.id}-name`}
|
||||
label="Name"
|
||||
placeholder="Name"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhooks?.at(index)?.name}
|
||||
helperText={errors?.webhooks?.at(index)?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`webhooks.${index}.description`)}
|
||||
id={`${field.id}-description`}
|
||||
label="Description"
|
||||
placeholder="Description"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhooks?.at(index)?.description}
|
||||
helperText={errors?.webhooks?.at(index)?.description?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[22px]',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`webhooks.${index}.URL`)}
|
||||
id={`${field.id}-URL`}
|
||||
label="URL"
|
||||
placeholder="URL"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhooks?.at(index)?.URL}
|
||||
helperText={errors?.webhooks?.at(index)?.URL?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[22px]',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ArgumentsFormSection nestedField="webhooks" nestIndex={index} />
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="h-10 self-end"
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{index < fields.length - 1 && (
|
||||
<Divider className="h-px" sx={{ background: 'grey.200' }} />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as WebhooksDataSourcesFormSection } from './WebhooksDataSourcesFormSection';
|
||||
1
dashboard/src/features/ai/AssistantForm/index.ts
Normal file
1
dashboard/src/features/ai/AssistantForm/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AssistantForm } from './AssistantForm';
|
||||
158
dashboard/src/features/ai/AssistantsList/AssistantsList.tsx
Normal file
158
dashboard/src/features/ai/AssistantsList/AssistantsList.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { AssistantForm } from '@/features/ai/AssistantForm';
|
||||
import { DeleteAssistantModal } from '@/features/ai/DeleteAssistantModal';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants';
|
||||
|
||||
interface AssistantsListProps {
|
||||
/**
|
||||
* The run services fetched from entering the users page.
|
||||
*/
|
||||
assistants: Assistant[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating a service.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
onCreateOrUpdate?: () => Promise<any>;
|
||||
|
||||
/**
|
||||
* Function to be called after a successful delete action.
|
||||
*
|
||||
*/
|
||||
onDelete?: () => Promise<any>;
|
||||
}
|
||||
|
||||
export default function AssistantsList({
|
||||
assistants,
|
||||
onCreateOrUpdate,
|
||||
onDelete,
|
||||
}: AssistantsListProps) {
|
||||
const { openDrawer, openDialog, closeDialog } = useDialog();
|
||||
|
||||
const viewAssistant = async (assistant: Assistant) => {
|
||||
openDrawer({
|
||||
title: `Edit ${assistant?.name ?? 'unset'}`,
|
||||
component: (
|
||||
<AssistantForm
|
||||
assistantId={assistant.assistantID}
|
||||
initialData={{
|
||||
...assistant,
|
||||
}}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAssistant = async (assistant: Assistant) => {
|
||||
openDialog({
|
||||
component: (
|
||||
<DeleteAssistantModal
|
||||
assistant={assistant}
|
||||
close={closeDialog}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col">
|
||||
{assistants.map((assistant) => (
|
||||
<Box
|
||||
key={assistant.assistantID}
|
||||
className="flex h-[64px] w-full cursor-pointer items-center justify-between space-x-4 border-b-1 px-4 py-2 transition-colors"
|
||||
sx={{
|
||||
[`&:hover`]: {
|
||||
backgroundColor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={() => viewAssistant(assistant)}
|
||||
className="flex w-full flex-row justify-between"
|
||||
sx={{ backgroundColor: 'transparent' }}
|
||||
>
|
||||
<div className="flex flex-1 flex-row items-center space-x-4">
|
||||
<span className="text-3xl">🤖</span>
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
{assistant?.name ?? 'unset'}
|
||||
</Text>
|
||||
<div className="hidden flex-row items-center space-x-2 md:flex">
|
||||
<Text variant="subtitle1" className="font-mono text-xs">
|
||||
{assistant.assistantID}
|
||||
</Text>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
copy(assistant.assistantID, 'Assistant Id');
|
||||
event.stopPropagation();
|
||||
}}
|
||||
aria-label="Service Id"
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
aria-label="More options"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-auto' }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={() => viewAssistant(assistant)}
|
||||
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
<Text className="font-medium">View {assistant?.name}</Text>
|
||||
</Dropdown.Item>
|
||||
<Divider component="li" />
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
sx={{ color: 'error.main' }}
|
||||
onClick={() => deleteAssistant(assistant)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<Text className="font-medium" color="error">
|
||||
Delete {assistant?.name}
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/features/ai/AssistantsList/index.ts
Normal file
1
dashboard/src/features/ai/AssistantsList/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AssistantsList } from './AssistantsList';
|
||||
@@ -0,0 +1,330 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { ArrowsClockwise } from '@/components/ui/v2/icons/ArrowsClockwise';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import {
|
||||
useInsertGraphiteAutoEmbeddingsConfigurationMutation,
|
||||
useUpdateGraphiteAutoEmbeddingsConfigurationMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import {
|
||||
ApolloClient,
|
||||
HttpLink,
|
||||
InMemoryCache,
|
||||
type ApolloError,
|
||||
} from '@apollo/client';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
name: Yup.string().required('The name is required.'),
|
||||
schemaName: Yup.string().required('The schema is required'),
|
||||
tableName: Yup.string().required('The table is required'),
|
||||
columnName: Yup.string().required('The column is required'),
|
||||
query: Yup.string(),
|
||||
mutation: Yup.string(),
|
||||
});
|
||||
|
||||
export type AutoEmbeddingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export interface AutoEmbeddingsFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
|
||||
*/
|
||||
autoEmbeddingsId?: string;
|
||||
|
||||
/**
|
||||
* if there is initialData then it's an update operation
|
||||
*/
|
||||
initialData?: AutoEmbeddingsFormValues;
|
||||
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
*/
|
||||
onCancel?: VoidFunction;
|
||||
/**
|
||||
* Function to be called when the submit is successful.
|
||||
*/
|
||||
onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
|
||||
}
|
||||
|
||||
export default function AutoEmbeddingsForm({
|
||||
autoEmbeddingsId,
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
location,
|
||||
}: AutoEmbeddingsFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
'graphql',
|
||||
);
|
||||
|
||||
const client = new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: new HttpLink({
|
||||
uri: serviceUrl,
|
||||
headers: {
|
||||
'x-hasura-admin-secret':
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: currentProject?.config?.hasura.adminSecret,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [insertGraphiteAutoEmbeddingsConfiguration] =
|
||||
useInsertGraphiteAutoEmbeddingsConfigurationMutation({
|
||||
client,
|
||||
});
|
||||
|
||||
const [updateGraphiteAutoEmbeddingsConfiguration] =
|
||||
useUpdateGraphiteAutoEmbeddingsConfigurationMutation({ client });
|
||||
|
||||
const form = useForm<AutoEmbeddingsFormValues>({
|
||||
defaultValues: initialData,
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting, dirtyFields },
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateAutoEmbeddings = async (
|
||||
values: AutoEmbeddingsFormValues,
|
||||
) => {
|
||||
// If the autoEmbeddingsId is set then we do an update
|
||||
if (autoEmbeddingsId) {
|
||||
await updateGraphiteAutoEmbeddingsConfiguration({
|
||||
variables: {
|
||||
id: autoEmbeddingsId,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await insertGraphiteAutoEmbeddingsConfiguration({
|
||||
variables: values,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: AutoEmbeddingsFormValues) => {
|
||||
try {
|
||||
await toast.promise(
|
||||
createOrUpdateAutoEmbeddings(values),
|
||||
{
|
||||
loading: 'Configuring the Auto-Embeddings...',
|
||||
success: `The Auto-Embeddings has been configured successfully.`,
|
||||
error: (arg: ApolloError) => {
|
||||
// we need to get the internal error message from the GraphQL error
|
||||
const { internal } = arg.graphQLErrors[0]?.extensions || {};
|
||||
const { message } = (internal as Record<string, any>)?.error || {};
|
||||
|
||||
// we use the default Apollo error message if we can't find the
|
||||
// internal error message
|
||||
return (
|
||||
message ||
|
||||
arg.message ||
|
||||
'An error occurred while configuring the Auto-Embeddings. Please try again.'
|
||||
);
|
||||
},
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
onSubmit?.();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col gap-4 overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-1 flex-col space-y-6 overflow-auto px-6">
|
||||
<Input
|
||||
{...register('name')}
|
||||
id="name"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Name</Text>
|
||||
<Tooltip title="Name of the Auto-Embeddings">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.name}
|
||||
helperText={errors?.name?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
<Input
|
||||
{...register('schemaName')}
|
||||
id="schemaName"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Schema</Text>
|
||||
<Tooltip title={<span>Schema where the table belongs to</span>}>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.schemaName}
|
||||
helperText={errors?.schemaName?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register('tableName')}
|
||||
id="tableName"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Table</Text>
|
||||
<Tooltip title="Table Name">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.tableName}
|
||||
helperText={errors?.tableName?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register('columnName')}
|
||||
id="columnName"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Column</Text>
|
||||
<Tooltip title="Column name">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.columnName}
|
||||
helperText={errors?.columnName?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register('query')}
|
||||
id="query"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Query</Text>
|
||||
<Tooltip title="Query">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.query}
|
||||
helperText={errors?.query?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
rows={6}
|
||||
/>
|
||||
<Input
|
||||
{...register('mutation')}
|
||||
id="mutation"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Mutation</Text>
|
||||
<Tooltip title="Mutation">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
placeholder=""
|
||||
hideEmptyHelperText
|
||||
error={!!errors.mutation}
|
||||
helperText={errors?.mutation?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Box className="flex w-full flex-row justify-between rounded border-t px-6 py-4">
|
||||
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
startIcon={autoEmbeddingsId ? <ArrowsClockwise /> : <PlusIcon />}
|
||||
>
|
||||
{autoEmbeddingsId ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/features/ai/AutoEmbeddingsForm/index.ts
Normal file
1
dashboard/src/features/ai/AutoEmbeddingsForm/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AutoEmbeddingsForm } from './AutoEmbeddingsForm';
|
||||
@@ -0,0 +1,172 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { Dropdown } from '@/components/ui/v2/Dropdown';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { CubeIcon } from '@/components/ui/v2/icons/CubeIcon';
|
||||
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
|
||||
import { EmbeddingsIcon } from '@/components/ui/v2/icons/EmbeddingsIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { AutoEmbeddingsForm } from '@/features/ai/AutoEmbeddingsForm';
|
||||
import { DeleteAutoEmbeddingsModal } from '@/features/ai/DeleteAutoEmbeddingsModal';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import type { AutoEmbeddingsConfiguration } from 'pages/[workspaceSlug]/[appSlug]/ai/auto-embeddings';
|
||||
|
||||
interface AutoEmbeddingsConfigurationsListProps {
|
||||
/**
|
||||
* The run services fetched from entering the users page.
|
||||
*/
|
||||
autoEmbeddingsConfigurations: AutoEmbeddingsConfiguration[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating a service.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
onCreateOrUpdate?: () => Promise<any>;
|
||||
|
||||
/**
|
||||
* Function to be called after a successful delete action.
|
||||
*
|
||||
*/
|
||||
onDelete?: () => Promise<any>;
|
||||
}
|
||||
|
||||
export default function AutoEmbeddingsList({
|
||||
autoEmbeddingsConfigurations,
|
||||
onCreateOrUpdate,
|
||||
onDelete,
|
||||
}: AutoEmbeddingsConfigurationsListProps) {
|
||||
const { openDrawer, openDialog, closeDialog } = useDialog();
|
||||
|
||||
const viewAutoEmbeddingsConfiguration = async (
|
||||
autoEmbeddingsConfiguration: AutoEmbeddingsConfiguration,
|
||||
) => {
|
||||
openDrawer({
|
||||
title: (
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<CubeIcon className="h-5 w-5" />
|
||||
<Text>Edit {autoEmbeddingsConfiguration?.name ?? 'unset'}</Text>
|
||||
</Box>
|
||||
),
|
||||
component: (
|
||||
<AutoEmbeddingsForm
|
||||
autoEmbeddingsId={autoEmbeddingsConfiguration.id}
|
||||
initialData={{
|
||||
...autoEmbeddingsConfiguration,
|
||||
}}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAutoEmbeddingsConfiguration = async (
|
||||
autoEmbeddingsConfiguration: AutoEmbeddingsConfiguration,
|
||||
) => {
|
||||
openDialog({
|
||||
component: (
|
||||
<DeleteAutoEmbeddingsModal
|
||||
autoEmbeddingsConfiguration={autoEmbeddingsConfiguration}
|
||||
close={closeDialog}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col">
|
||||
{autoEmbeddingsConfigurations.map((autoEmbeddingsConfiguration) => (
|
||||
<Box
|
||||
key={autoEmbeddingsConfiguration.id}
|
||||
className="flex h-[64px] w-full cursor-pointer items-center justify-between space-x-4 border-b-1 px-4 py-2 transition-colors"
|
||||
sx={{
|
||||
[`&:hover`]: {
|
||||
backgroundColor: 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={() =>
|
||||
viewAutoEmbeddingsConfiguration(autoEmbeddingsConfiguration)
|
||||
}
|
||||
className="flex w-full flex-row justify-between"
|
||||
sx={{
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-1 flex-row items-center space-x-4">
|
||||
<EmbeddingsIcon className="h-5 w-5" />
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
{autoEmbeddingsConfiguration?.name ?? 'unset'}
|
||||
</Text>
|
||||
<Tooltip title={autoEmbeddingsConfiguration.updatedAt}>
|
||||
<span className="hidden cursor-pointer text-sm text-slate-500 xs+:flex">
|
||||
Updated{' '}
|
||||
{formatDistanceToNow(
|
||||
new Date(autoEmbeddingsConfiguration.updatedAt),
|
||||
)}{' '}
|
||||
ago
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
aria-label="More options"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-auto' }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={() =>
|
||||
viewAutoEmbeddingsConfiguration(autoEmbeddingsConfiguration)
|
||||
}
|
||||
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
<Text className="font-medium">
|
||||
View {autoEmbeddingsConfiguration?.name}
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
<Divider component="li" />
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
sx={{ color: 'error.main' }}
|
||||
onClick={() =>
|
||||
deleteAutoEmbeddingsConfiguration(autoEmbeddingsConfiguration)
|
||||
}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<Text className="font-medium" color="error">
|
||||
Delete {autoEmbeddingsConfiguration?.name}
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/features/ai/AutoEmbeddingsList/index.ts
Normal file
1
dashboard/src/features/ai/AutoEmbeddingsList/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AutoEmbeddingsList } from './AutoEmbeddingsList';
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { useDeleteAssistantMutation } from '@/utils/__generated__/graphite.graphql';
|
||||
import {
|
||||
ApolloClient,
|
||||
HttpLink,
|
||||
InMemoryCache,
|
||||
type ApolloError,
|
||||
} from '@apollo/client';
|
||||
import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface DeleteAssistantModalProps {
|
||||
assistant: Assistant;
|
||||
onDelete?: () => Promise<any>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export default function DeleteAssistantModal({
|
||||
assistant,
|
||||
onDelete,
|
||||
close,
|
||||
}: DeleteAssistantModalProps) {
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
'graphql',
|
||||
);
|
||||
|
||||
const client = new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: new HttpLink({
|
||||
uri: serviceUrl,
|
||||
headers: {
|
||||
'x-hasura-admin-secret':
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: currentProject?.config?.hasura.adminSecret,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [deleteAssistantMutation] = useDeleteAssistantMutation({
|
||||
client,
|
||||
});
|
||||
|
||||
const deleteAssistant = async () => {
|
||||
await deleteAssistantMutation({
|
||||
variables: {
|
||||
id: assistant.assistantID,
|
||||
},
|
||||
});
|
||||
await onDelete?.();
|
||||
close();
|
||||
};
|
||||
|
||||
async function handleClick() {
|
||||
setLoadingRemove(true);
|
||||
|
||||
await toast.promise(
|
||||
deleteAssistant(),
|
||||
{
|
||||
loading: 'Deleting the assistant...',
|
||||
success: `The Assistant has been deleted successfully.`,
|
||||
error: (arg: ApolloError) => {
|
||||
// we need to get the internal error message from the GraphQL error
|
||||
const { internal } = arg.graphQLErrors[0]?.extensions || {};
|
||||
const { message } = (internal as Record<string, any>)?.error || {};
|
||||
|
||||
// we use the default Apollo error message if we can't find the
|
||||
// internal error message
|
||||
return (
|
||||
message ||
|
||||
arg.message ||
|
||||
'An error occurred while deleting the Assistant. Please try again.'
|
||||
);
|
||||
},
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
Delete Assistant {assistant?.name}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
Are you sure you want to delete this Assistant?
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="font-bold"
|
||||
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
|
||||
>
|
||||
This cannot be undone.
|
||||
</Text>
|
||||
|
||||
<Box className="my-4">
|
||||
<Checkbox
|
||||
id="accept-1"
|
||||
label={`I'm sure I want to delete ${assistant?.name}`}
|
||||
className="py-2"
|
||||
checked={remove}
|
||||
onChange={(_event, checked) => setRemove(checked)}
|
||||
aria-label="Confirm Delete Assistant"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
color="error"
|
||||
onClick={handleClick}
|
||||
disabled={!remove}
|
||||
loading={loadingRemove}
|
||||
>
|
||||
Delete Assistant
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/features/ai/DeleteAssistantModal/index.ts
Normal file
1
dashboard/src/features/ai/DeleteAssistantModal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as DeleteAssistantModal } from './DeleteAssistantModal';
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { useDeleteGraphiteAutoEmbeddingsConfigurationMutation } from '@/utils/__generated__/graphite.graphql';
|
||||
import {
|
||||
ApolloClient,
|
||||
HttpLink,
|
||||
InMemoryCache,
|
||||
type ApolloError,
|
||||
} from '@apollo/client';
|
||||
import { type AutoEmbeddingsConfiguration } from 'pages/[workspaceSlug]/[appSlug]/ai/auto-embeddings';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface DeleteAutoEmbeddingsModalProps {
|
||||
autoEmbeddingsConfiguration: AutoEmbeddingsConfiguration;
|
||||
onDelete?: () => Promise<any>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export default function DeleteAutoEmbeddingsModal({
|
||||
autoEmbeddingsConfiguration,
|
||||
onDelete,
|
||||
close,
|
||||
}: DeleteAutoEmbeddingsModalProps) {
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
'graphql',
|
||||
);
|
||||
|
||||
const client = new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: new HttpLink({
|
||||
uri: serviceUrl,
|
||||
headers: {
|
||||
'x-hasura-admin-secret':
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: currentProject?.config?.hasura.adminSecret,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const [deleteAutoEmbeddingsConfiguration] =
|
||||
useDeleteGraphiteAutoEmbeddingsConfigurationMutation({
|
||||
client,
|
||||
});
|
||||
|
||||
const deleteAutoEmbeddingsConfig = async () => {
|
||||
await deleteAutoEmbeddingsConfiguration({
|
||||
variables: {
|
||||
id: autoEmbeddingsConfiguration.id,
|
||||
},
|
||||
});
|
||||
await onDelete?.();
|
||||
close();
|
||||
};
|
||||
|
||||
async function handleClick() {
|
||||
setLoadingRemove(true);
|
||||
|
||||
await toast.promise(
|
||||
deleteAutoEmbeddingsConfig(),
|
||||
{
|
||||
loading: 'Deleting Auto-Embeddings Configuration...',
|
||||
success: `The Auto-Embeddings Configuration has been deleted successfully.`,
|
||||
error: (arg: ApolloError) => {
|
||||
// we need to get the internal error message from the GraphQL error
|
||||
const { internal } = arg.graphQLErrors[0]?.extensions || {};
|
||||
const { message } = (internal as Record<string, any>)?.error || {};
|
||||
|
||||
// we use the default Apollo error message if we can't find the
|
||||
// internal error message
|
||||
return (
|
||||
message ||
|
||||
arg.message ||
|
||||
'An error occurred while deleting the Auto-Embeddings Configuration. Please try again.'
|
||||
);
|
||||
},
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
Delete Auto-Embeddings Configuration{' '}
|
||||
{autoEmbeddingsConfiguration?.name}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
Are you sure you want to delete this Auto-Embeddings Configuration?
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="font-bold"
|
||||
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
|
||||
>
|
||||
This cannot be undone.
|
||||
</Text>
|
||||
|
||||
<Box className="my-4">
|
||||
<Checkbox
|
||||
id="accept-1"
|
||||
label={`I'm sure I want to delete ${autoEmbeddingsConfiguration?.name}`}
|
||||
className="py-2"
|
||||
checked={remove}
|
||||
onChange={(_event, checked) => setRemove(checked)}
|
||||
aria-label="Confirm Delete Auto-Embeddings Configuration"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
color="error"
|
||||
onClick={handleClick}
|
||||
disabled={!remove}
|
||||
loading={loadingRemove}
|
||||
>
|
||||
Delete Auto-Embeddings Configuration
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DeleteAutoEmbeddingsModal } from './DeleteAutoEmbeddingsModal';
|
||||
219
dashboard/src/features/ai/DevAssistant/DevAssistant.tsx
Normal file
219
dashboard/src/features/ai/DevAssistant/DevAssistant.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { ArrowUpIcon } from '@/components/ui/v2/icons/ArrowUpIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { MessagesList } from '@/features/ai/DevAssistant/components/MessagesList';
|
||||
import {
|
||||
messagesState,
|
||||
projectMessagesState,
|
||||
sessionIDState,
|
||||
} from '@/features/ai/DevAssistant/state';
|
||||
import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import {
|
||||
useSendDevMessageMutation,
|
||||
useStartDevSessionMutation,
|
||||
type SendDevMessageMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
const MAX_THREAD_LENGTH = 50;
|
||||
|
||||
export type Message = Omit<
|
||||
SendDevMessageMutation['graphite']['sendDevMessage']['messages'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function DevAssistant() {
|
||||
const { currentProject, currentWorkspace } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [userInput, setUserInput] = useState('');
|
||||
const setMessages = useSetRecoilState(messagesState);
|
||||
const messages = useRecoilValue(projectMessagesState(currentProject.id));
|
||||
const [storedSessionID, setStoredSessionID] = useRecoilState(sessionIDState);
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
const [startDevSession] = useStartDevSessionMutation({ client: adminClient });
|
||||
const [sendDevMessage] = useSendDevMessageMutation({ client: adminClient });
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setUserInput('');
|
||||
|
||||
let sessionID = storedSessionID;
|
||||
const lastMessage = messages.slice(1).pop(); // The first message is a welcome message, so we exclude it
|
||||
|
||||
let hasBeenAnHourSinceLastMessage = false;
|
||||
if (lastMessage) {
|
||||
hasBeenAnHourSinceLastMessage =
|
||||
new Date().getTime() - new Date(lastMessage.createdAt).getTime() >
|
||||
60 * 60 * 1000;
|
||||
}
|
||||
|
||||
const $messages = [
|
||||
...messages,
|
||||
{
|
||||
id: String(new Date().getTime()),
|
||||
message: userInput,
|
||||
createdAt: null,
|
||||
role: 'user',
|
||||
projectId: currentProject.id,
|
||||
},
|
||||
];
|
||||
|
||||
setMessages($messages);
|
||||
|
||||
if (!sessionID || hasBeenAnHourSinceLastMessage) {
|
||||
const sessionRes = await startDevSession({ client: adminClient });
|
||||
sessionID = sessionRes?.data?.graphite?.startDevSession?.sessionID;
|
||||
setStoredSessionID(sessionID);
|
||||
}
|
||||
|
||||
if (!sessionID) {
|
||||
throw new Error('Failed to start a new session');
|
||||
}
|
||||
|
||||
const {
|
||||
data: {
|
||||
graphite: { sendDevMessage: { messages: newMessages } = {} } = {},
|
||||
} = {},
|
||||
} = await sendDevMessage({
|
||||
variables: {
|
||||
message: userInput,
|
||||
sessionId: sessionID || '',
|
||||
prevMessageID: !hasBeenAnHourSinceLastMessage
|
||||
? lastMessage?.id || ''
|
||||
: '',
|
||||
},
|
||||
});
|
||||
|
||||
let thread = [
|
||||
// remove the temp messages of the user input while we wait for the dev assistant to respond
|
||||
...$messages.filter((item) => item.createdAt),
|
||||
...newMessages
|
||||
|
||||
// remove empty messages
|
||||
.filter((item) => item.message)
|
||||
|
||||
// add the currentProject.id to the new messages
|
||||
.map((item) => ({ ...item, projectId: currentProject.id })),
|
||||
];
|
||||
|
||||
if (thread.length > MAX_THREAD_LENGTH) {
|
||||
thread = thread.slice(thread.length - MAX_THREAD_LENGTH); // keep the thread at a max length of MAX_THREAD_LENGTH
|
||||
}
|
||||
|
||||
setMessages(thread);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
'Failed to send the message to graphite. Please try again later.',
|
||||
{
|
||||
style: getToastStyleProps().style,
|
||||
...getToastStyleProps().error,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget.closest('form');
|
||||
if (form) {
|
||||
form.dispatchEvent(
|
||||
new Event('submit', { bubbles: true, cancelable: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (currentProject.plan.isFree) {
|
||||
return (
|
||||
<Box className="p-4">
|
||||
<UpgradeToProBanner
|
||||
title="Upgrade to Nhost Pro."
|
||||
description={
|
||||
<Text>
|
||||
Graphite is an addon to the Pro plan. To unlock it, please upgrade
|
||||
to Pro first.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentProject.plan.isFree && !currentProject.config?.ai) {
|
||||
return (
|
||||
<Box className="p-4">
|
||||
<Alert className="grid w-full grid-flow-col place-content-between items-center gap-2">
|
||||
<Text className="grid grid-flow-row justify-items-start gap-0.5">
|
||||
<Text component="span">
|
||||
To enable graphite, configure the service first in{' '}
|
||||
<Link
|
||||
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/ai`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
AI Settings
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Text>
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-auto">
|
||||
<MessagesList loading={loading} />
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Box className="relative flex w-full flex-row justify-between p-2">
|
||||
<Input
|
||||
value={userInput}
|
||||
onChange={(event) => {
|
||||
const { value } = event.target;
|
||||
setUserInput(value);
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Ask graphite anything!"
|
||||
className="w-full"
|
||||
required
|
||||
slotProps={{
|
||||
input: { className: 'w-full rounded-none border-none' },
|
||||
}}
|
||||
multiline
|
||||
maxRows={7}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={!userInput || loading}
|
||||
color="primary"
|
||||
aria-label="Send"
|
||||
type="submit"
|
||||
className="absolute right-2 h-10 w-12 self-end rounded-xl"
|
||||
>
|
||||
{loading ? <ActivityIndicator /> : <ArrowUpIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
|
||||
export default function LoadingAssistantMessage() {
|
||||
return (
|
||||
<Box className="flex flex-col space-y-4 border-t p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<GraphiteIcon />
|
||||
<Text className="font-bold">Assistant</Text>
|
||||
</div>
|
||||
<div className="flex space-x-1">
|
||||
<Box
|
||||
className="h-1.5 w-1.5 animate-blinking rounded-full"
|
||||
sx={{ backgroundColor: 'grey.600' }}
|
||||
/>
|
||||
<Box
|
||||
className="h-1.5 w-1.5 animate-blinking rounded-full animate-delay-150"
|
||||
sx={{ backgroundColor: 'grey.600' }}
|
||||
/>
|
||||
<Box
|
||||
className="h-1.5 w-1.5 animate-blinking rounded-full animate-delay-300"
|
||||
sx={{ backgroundColor: 'grey.600' }}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as LoadingAssistantMessage } from './LoadingAssistantMessage';
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Avatar } from '@/components/ui/v2/Avatar';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { type Message } from '@/features/ai/DevAssistant';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { onlyText } from 'react-children-utilities';
|
||||
import Markdown, { type ExtraProps } from 'react-markdown';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import remarkGFM from 'remark-gfm';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { type ClassAttributes, type HTMLAttributes } from 'react';
|
||||
|
||||
function PreComponent(
|
||||
props: ClassAttributes<HTMLElement> &
|
||||
HTMLAttributes<HTMLElement> &
|
||||
ExtraProps,
|
||||
) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<pre>{children}</pre>
|
||||
<IconButton
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
padding: 0.5,
|
||||
backgroundColor: 'grey.100',
|
||||
}}
|
||||
color="warning"
|
||||
variant="contained"
|
||||
className="absolute top-2 right-2 hidden group-hover:flex"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(onlyText(children), 'Snippet');
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MessageBox({ message }: { message: Message }) {
|
||||
const theme = useTheme();
|
||||
const user = useUserData();
|
||||
const isUserMessage = message.role === 'user';
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="flex flex-col space-y-4 border-t p-4 first:border-t-0"
|
||||
sx={{
|
||||
backgroundColor: isUserMessage && 'background.default',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{message.role === 'assistant' ? (
|
||||
<>
|
||||
<GraphiteIcon />
|
||||
<Text className="font-bold">Assistant</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Avatar
|
||||
className="h-7 w-7 rounded-full"
|
||||
alt={user?.displayName}
|
||||
src={user?.avatarUrl}
|
||||
>
|
||||
{user?.displayName || 'local'}
|
||||
</Avatar>
|
||||
<Text className="font-bold">
|
||||
{user?.displayName || 'local'} (You)
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Markdown
|
||||
className={twMerge(
|
||||
'prose',
|
||||
theme.palette.mode === 'dark' && 'prose-invert',
|
||||
)}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
remarkPlugins={[remarkGFM]}
|
||||
components={{
|
||||
pre: PreComponent,
|
||||
}}
|
||||
>
|
||||
{message.message}
|
||||
</Markdown>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as MessageBox } from './MessageBox';
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { LoadingAssistantMessage } from '@/features/ai/DevAssistant/components/LoadingAssistantMessage';
|
||||
import { MessageBox } from '@/features/ai/DevAssistant/components/MessageBox';
|
||||
import { projectMessagesState } from '@/features/ai/DevAssistant/state';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
interface MessagesListProps {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
function MessagesList({ loading }: MessagesListProps) {
|
||||
const bottomElement = useRef(null);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const messages = useRecoilValue(projectMessagesState(currentProject.id));
|
||||
|
||||
const scrollToBottom = () =>
|
||||
bottomElement?.current?.scrollIntoView({ behavior: 'instant' });
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages, loading]);
|
||||
|
||||
return (
|
||||
<Box className="flex grow flex-col overflow-auto border-y">
|
||||
{messages.map((message) => (
|
||||
<MessageBox key={message.id} message={message} />
|
||||
))}
|
||||
{loading && <LoadingAssistantMessage />}
|
||||
<div ref={bottomElement} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MessagesList);
|
||||
@@ -0,0 +1 @@
|
||||
export { default as MessagesList } from './MessagesList';
|
||||
2
dashboard/src/features/ai/DevAssistant/index.ts
Normal file
2
dashboard/src/features/ai/DevAssistant/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './DevAssistant';
|
||||
export { default as DevAssistant } from './DevAssistant';
|
||||
5
dashboard/src/features/ai/DevAssistant/state/index.ts
Normal file
5
dashboard/src/features/ai/DevAssistant/state/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './messages';
|
||||
export { default as messagesState } from './messages';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
export { default as projectMessagesState } from './projectMessages';
|
||||
export { default as sessionIDState } from './session';
|
||||
23
dashboard/src/features/ai/DevAssistant/state/messages.ts
Normal file
23
dashboard/src/features/ai/DevAssistant/state/messages.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type Message } from '@/features/ai/DevAssistant';
|
||||
import { persistAtom } from '@/utils/recoil';
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export interface ProjectMessage extends Message {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
const messagesState = atom<ProjectMessage[]>({
|
||||
key: 'messages',
|
||||
default: [
|
||||
{
|
||||
id: '0',
|
||||
message:
|
||||
"Hi, I'm your personal Nhost AI assistant. I'm here to help answer questions, assist with tasks, provide information, or just have a conversation about GraphQL!",
|
||||
role: 'assistant',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
effects: [persistAtom],
|
||||
});
|
||||
|
||||
export default messagesState;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user