Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9f814b27 | ||
|
|
49b08e9e6f | ||
|
|
a879051454 | ||
|
|
a0439cd7f4 | ||
|
|
bc436da263 | ||
|
|
f912c5eb5d | ||
|
|
84f0b936ca | ||
|
|
72d34fcc4f | ||
|
|
a7dd23141c | ||
|
|
c54e6b0b34 | ||
|
|
f8a7b86ab7 | ||
|
|
352d3a1647 | ||
|
|
f04fd8bea2 | ||
|
|
2d587a7d1b | ||
|
|
b7b7687608 | ||
|
|
840069b935 | ||
|
|
eaa85bb9c4 | ||
|
|
cc26601c06 | ||
|
|
643d160748 | ||
|
|
a71756a6ec | ||
|
|
db02f68095 | ||
|
|
a0f3db812e | ||
|
|
8c393f848b | ||
|
|
cf591a5a0b | ||
|
|
b45df68ece | ||
|
|
fdaaacc3af | ||
|
|
94ac24289e | ||
|
|
1d816b0677 | ||
|
|
9b413f25fb | ||
|
|
e08fba1b76 | ||
|
|
961c398690 | ||
|
|
9954a74dfd | ||
|
|
faf4164818 | ||
|
|
9254b9096c | ||
|
|
aa8029a854 | ||
|
|
9edc6e4b27 | ||
|
|
d842f93a19 | ||
|
|
0f82f076f7 | ||
|
|
f00d7275e5 | ||
|
|
5fdf8431da | ||
|
|
af0c69764f | ||
|
|
4ed8d6b90c | ||
|
|
a88e304c5d | ||
|
|
847c7630ec | ||
|
|
4aaa2faf9a | ||
|
|
292c7fd7ea | ||
|
|
87263dc6d9 | ||
|
|
8a4fba056b | ||
|
|
30131c97a3 | ||
|
|
b02a3379da | ||
|
|
8f46da0907 | ||
|
|
6e5550ba32 | ||
|
|
edd2346939 | ||
|
|
d5262a1968 | ||
|
|
d9cc0f82c1 | ||
|
|
34455e10e4 | ||
|
|
d9ccc72a38 | ||
|
|
e2e1eff289 | ||
|
|
85b13686a4 | ||
|
|
c8bf571f7a | ||
|
|
731f2e12fb | ||
|
|
3acfde97df | ||
|
|
0e8a71c760 | ||
|
|
2e869310b8 | ||
|
|
f5cf8e25f5 | ||
|
|
9c836ed39b | ||
|
|
32b731f60e | ||
|
|
708234118a | ||
|
|
798b9aed8f | ||
|
|
d55196e06f | ||
|
|
3d130bd8a5 | ||
|
|
278964500b | ||
|
|
644de5f34a | ||
|
|
5428999d46 | ||
|
|
35a9c081b5 | ||
|
|
3295423804 | ||
|
|
19c5f4c8c3 | ||
|
|
6c4717d5f6 | ||
|
|
1cb730764a | ||
|
|
95403fb56b | ||
|
|
2b4727db0d | ||
|
|
59c8c14e6c | ||
|
|
fbbd9b9092 | ||
|
|
1f804a725e | ||
|
|
5ba82415ad | ||
|
|
c44325ba7f | ||
|
|
78626253ee | ||
|
|
1a2009b615 | ||
|
|
eca4bb0cb5 | ||
|
|
866b29db68 | ||
|
|
07b2ddc29e | ||
|
|
6db0aff073 | ||
|
|
2bb76a1b2f | ||
|
|
e91e01adf3 | ||
|
|
c5870b96d5 | ||
|
|
e772a35243 | ||
|
|
6047163150 | ||
|
|
4c5bb66c3d | ||
|
|
d9dbfc6d6e | ||
|
|
b462968650 | ||
|
|
2c553ae1d2 | ||
|
|
e04ba2d163 | ||
|
|
a30417ab42 | ||
|
|
83373d0520 | ||
|
|
b59cad5389 | ||
|
|
58b102b921 | ||
|
|
32edb64da4 | ||
|
|
eecdb7e552 | ||
|
|
fd51af97fa | ||
|
|
e5028c95b3 | ||
|
|
5d20cf26ab | ||
|
|
6457984dd4 | ||
|
|
8749edfaf0 | ||
|
|
78a2e5d106 | ||
|
|
6ec6d582d8 | ||
|
|
6a079704bb | ||
|
|
65eb22bbe0 | ||
|
|
549044f973 | ||
|
|
df56799ab9 | ||
|
|
ba8011a22e | ||
|
|
1a69af6360 | ||
|
|
b718c02787 | ||
|
|
4fa0e08574 | ||
|
|
9681730263 | ||
|
|
b03f951352 | ||
|
|
34b56bcadc | ||
|
|
8606b5e5de | ||
|
|
6c7fefe4ed | ||
|
|
9bb5362c5f | ||
|
|
a84f30c36d | ||
|
|
4cc3765e0d | ||
|
|
915d2e06ae | ||
|
|
95b832c1c6 | ||
|
|
1e0ddb248c | ||
|
|
635717ec92 | ||
|
|
db285d1867 | ||
|
|
00ebc4c270 | ||
|
|
d1b3d14457 | ||
|
|
6cd52cb31d | ||
|
|
310084a902 | ||
|
|
1b8c6167b7 | ||
|
|
e371596d15 | ||
|
|
183c13fe53 | ||
|
|
8d6078e2c1 | ||
|
|
4f17e84247 | ||
|
|
563cc8d03d | ||
|
|
09b5261ba2 | ||
|
|
ff9a7c33eb | ||
|
|
08b9cc16fc | ||
|
|
27c1e9f1da | ||
|
|
8d0cd1dc05 | ||
|
|
0d01a08304 | ||
|
|
4d91377259 | ||
|
|
fe25a5d170 | ||
|
|
c16d5c4246 | ||
|
|
40f4294cae | ||
|
|
1cc9f4e4d9 | ||
|
|
b282ff9883 | ||
|
|
06d35e1626 | ||
|
|
d184f3fd38 | ||
|
|
3d67cfaacd | ||
|
|
2ee2d9b905 | ||
|
|
2df8bc14dd | ||
|
|
2cb40b3885 | ||
|
|
536c27cb30 | ||
|
|
1df36a66b9 | ||
|
|
1ebb6b922b | ||
|
|
dc7cc431d2 | ||
|
|
b9a8328a48 | ||
|
|
5e14c15b92 | ||
|
|
3bcf3b1ed0 | ||
|
|
e7429ecd67 | ||
|
|
0c94472062 | ||
|
|
dbf9f86105 | ||
|
|
dfee81f5b0 | ||
|
|
40f879f671 | ||
|
|
c8685c4273 | ||
|
|
2b9fbf743a | ||
|
|
30d85e5712 | ||
|
|
ac9ff86e68 | ||
|
|
b6680be345 | ||
|
|
7839e4c197 | ||
|
|
daa57ed948 | ||
|
|
41137c09b2 | ||
|
|
05add10ba8 | ||
|
|
bf73a71523 | ||
|
|
91ab821090 | ||
|
|
32688ba320 | ||
|
|
288c6f88a3 | ||
|
|
0336170c8d | ||
|
|
03ced27d30 | ||
|
|
8e4bb89974 | ||
|
|
35aa5162ca | ||
|
|
92c2b2d6c1 | ||
|
|
5758a84e13 | ||
|
|
176f576979 | ||
|
|
9d18d55bbf | ||
|
|
961d827a52 | ||
|
|
365ae08123 | ||
|
|
0b6febbb82 | ||
|
|
6913bd0179 | ||
|
|
c607581767 | ||
|
|
34485a0cae | ||
|
|
05e2bbd211 | ||
|
|
766c073ec4 | ||
|
|
3faa6194f3 | ||
|
|
ddf9673a35 | ||
|
|
5fcf8ce7ba | ||
|
|
d10341ac2f | ||
|
|
2161ef4388 | ||
|
|
7870e83586 | ||
|
|
230753320a | ||
|
|
465bdd2945 | ||
|
|
4a36bd3157 | ||
|
|
e6fd57d0bb | ||
|
|
7c5516cd75 | ||
|
|
31321749b2 | ||
|
|
50e9a6b967 | ||
|
|
3a6d3071be | ||
|
|
9972c3cf9a | ||
|
|
f853a03491 | ||
|
|
c98b901902 | ||
|
|
b77f05709b | ||
|
|
28157c0b55 | ||
|
|
f3072e0447 | ||
|
|
105353c9f6 | ||
|
|
40117f777e | ||
|
|
2523d73625 | ||
|
|
74cef5adb1 | ||
|
|
452d1a2ad5 | ||
|
|
a29282801a | ||
|
|
fc1f4fc142 | ||
|
|
2b017490a3 | ||
|
|
ecb1ca4517 | ||
|
|
ba00e9afff | ||
|
|
9700551f41 | ||
|
|
24cb4c70a7 | ||
|
|
75ed049b01 | ||
|
|
faa2a2d965 | ||
|
|
a9c1d131a5 | ||
|
|
e8908351a2 | ||
|
|
3351115e3c | ||
|
|
2b279af093 | ||
|
|
a7f795795f | ||
|
|
85a996635f | ||
|
|
92d4e9ea12 | ||
|
|
c23e941ece | ||
|
|
c804f8b873 | ||
|
|
6c51fc91b4 | ||
|
|
e610acb9c7 | ||
|
|
d8573be7ac | ||
|
|
0a0c84b7f4 | ||
|
|
61a66644da | ||
|
|
1187f9d4cc | ||
|
|
906a76c545 | ||
|
|
205e16a96d | ||
|
|
4cf3804898 | ||
|
|
223203737f | ||
|
|
5675990504 | ||
|
|
9f88fbbcf2 | ||
|
|
c70b5b0ae8 | ||
|
|
a1c11fbb3c | ||
|
|
2b48a72f35 | ||
|
|
48b25e0838 | ||
|
|
1b4b66e36d | ||
|
|
30a7de5333 | ||
|
|
d4b8eebd64 | ||
|
|
8ea2053d37 | ||
|
|
68aa9a6b2d | ||
|
|
dbbe008e23 | ||
|
|
7e5521eb13 | ||
|
|
f9e0930336 | ||
|
|
dfbc3ed728 | ||
|
|
7ed92427a3 | ||
|
|
2df1dc1721 | ||
|
|
9a88523ea5 | ||
|
|
196757d36f | ||
|
|
28718680fa | ||
|
|
8fe3ed9bd1 | ||
|
|
0f298d6450 | ||
|
|
8d93d80e1e | ||
|
|
e6b9b5df3c | ||
|
|
711cd6cd44 | ||
|
|
81d830d99e | ||
|
|
b041c9014f | ||
|
|
3cacda687c | ||
|
|
7bca60269c | ||
|
|
bb921690bf | ||
|
|
440ce8a12b | ||
|
|
a86bc59e09 | ||
|
|
a091bf23b7 | ||
|
|
acd5dc982e | ||
|
|
6b3773ed76 | ||
|
|
29cfc4fb41 | ||
|
|
30e95b431c | ||
|
|
cb4fda937f | ||
|
|
728e337722 | ||
|
|
d558cdd49f | ||
|
|
47131633d0 | ||
|
|
85e81184e5 | ||
|
|
c78f43e32e | ||
|
|
79b5a33874 | ||
|
|
76a5a79482 | ||
|
|
f45736e318 | ||
|
|
8d30de1ff8 | ||
|
|
68f1895cda | ||
|
|
fc2676c33e | ||
|
|
c9359cc4e1 | ||
|
|
b5752024e7 | ||
|
|
df4f55b410 | ||
|
|
e71e2ef42c | ||
|
|
1e5c8612b8 | ||
|
|
75309fc53c | ||
|
|
e3b6ed27ba | ||
|
|
f474d63e88 | ||
|
|
287ea3e5a6 | ||
|
|
bcf883223b | ||
|
|
9c40021318 | ||
|
|
e818f1cd61 | ||
|
|
34ca3a65d6 | ||
|
|
1298b23844 | ||
|
|
f15cef4916 | ||
|
|
64e4a3278a | ||
|
|
791c62b583 | ||
|
|
456798ca15 | ||
|
|
7d333c6d93 | ||
|
|
7c275db662 | ||
|
|
c1004fc37a | ||
|
|
11212c7371 | ||
|
|
e9e509ab04 | ||
|
|
df42932e0c | ||
|
|
36d8e8c0cf | ||
|
|
75eb9f8560 | ||
|
|
c981b25ef9 | ||
|
|
638fcb3b9b | ||
|
|
37406484fc | ||
|
|
bebbe5896d | ||
|
|
f47d3c2cd6 | ||
|
|
3df6b451e7 | ||
|
|
4fd2a29ab3 | ||
|
|
a3b22d9056 | ||
|
|
ca4da6d485 | ||
|
|
959f265617 | ||
|
|
3cdc21f1be | ||
|
|
bc3801edc0 | ||
|
|
13741d0f0a | ||
|
|
9bba8440ca | ||
|
|
6f133ed7a0 | ||
|
|
55ea81c46a | ||
|
|
37443823b8 | ||
|
|
7c76125a0a | ||
|
|
dd16c3e8fe | ||
|
|
30a610511c | ||
|
|
e173269acd | ||
|
|
3235bf68e4 | ||
|
|
6cbc6546ac | ||
|
|
1490e9171d | ||
|
|
f30529e8a2 | ||
|
|
90a5fe946a | ||
|
|
1a198c28ee | ||
|
|
c90fb9dd08 | ||
|
|
9de6b4e2bc | ||
|
|
bea23a94bb | ||
|
|
5aae164865 | ||
|
|
bfae00b69b | ||
|
|
22f8a37f1f | ||
|
|
f6be4317cd | ||
|
|
1aeb4472bc | ||
|
|
20e32fad04 | ||
|
|
cb35dc591c | ||
|
|
738925d5ec | ||
|
|
b229720431 | ||
|
|
beffce8823 | ||
|
|
1607aba78f | ||
|
|
87ccc93ff9 | ||
|
|
91741c2ae5 | ||
|
|
e366741aa9 | ||
|
|
b105626f0b | ||
|
|
a37505b095 | ||
|
|
216e53166d | ||
|
|
800fd4792d | ||
|
|
7189c19d55 | ||
|
|
91694ff072 | ||
|
|
fcffb93a60 | ||
|
|
ad7912e6ba | ||
|
|
252961aca9 | ||
|
|
2d839285f2 | ||
|
|
d19bbf2b8e | ||
|
|
4b621a228f | ||
|
|
38a1d5e7a1 | ||
|
|
cc8949538f | ||
|
|
985ca3334d | ||
|
|
df69d06048 | ||
|
|
ef8f74d4e3 | ||
|
|
062df395c5 | ||
|
|
a25f292217 | ||
|
|
30fc14ed7f | ||
|
|
1850655467 | ||
|
|
ab3f867b5b | ||
|
|
b499b2879c | ||
|
|
18ea551118 | ||
|
|
42372ab111 | ||
|
|
5e23a2b79c | ||
|
|
c32cde6189 | ||
|
|
d2233d2e3e | ||
|
|
9da3d3ef55 | ||
|
|
cedd660fd9 | ||
|
|
b9fbfd2a25 | ||
|
|
4168c86734 | ||
|
|
024096631f | ||
|
|
e53901f6fd | ||
|
|
1a7842164d | ||
|
|
656c3c61a8 | ||
|
|
19303d43ea | ||
|
|
cd0b9cd7d6 | ||
|
|
5e8a0afd3b | ||
|
|
fac2f6f205 | ||
|
|
c3a2ca314a | ||
|
|
57f4db8d37 | ||
|
|
afece4df65 | ||
|
|
d9c2f31f4e | ||
|
|
134e547736 | ||
|
|
1542cf01a7 | ||
|
|
a63bcb5b4a | ||
|
|
deeb6a030c | ||
|
|
5e6aaa3caa | ||
|
|
9503f08dd8 | ||
|
|
fca39f1914 | ||
|
|
8d512b4719 | ||
|
|
9f204b36b0 | ||
|
|
a93edc1131 | ||
|
|
09b7dc8401 | ||
|
|
6f3c2df6ee | ||
|
|
a111903965 | ||
|
|
d62fe5f4ab | ||
|
|
289aa4fe32 | ||
|
|
cf84d1fde5 | ||
|
|
50986d4493 | ||
|
|
948e5e932b | ||
|
|
1574624f69 | ||
|
|
275f2384c0 | ||
|
|
b58fe4ae74 | ||
|
|
b1f4c64a21 | ||
|
|
4e10325eea | ||
|
|
6dea8b434b | ||
|
|
9242612f30 | ||
|
|
de3d38d891 | ||
|
|
0e4b371925 | ||
|
|
1794429cbe | ||
|
|
7e1a7066fc | ||
|
|
01ed3d8aaa | ||
|
|
ec25ebee35 | ||
|
|
c0ad2dfce2 | ||
|
|
bcea3a5e66 | ||
|
|
3ade8b5bd4 | ||
|
|
fac960b9ac | ||
|
|
045b506e2a | ||
|
|
9e6b826f6e | ||
|
|
cd17ecdadb | ||
|
|
f32892adaf | ||
|
|
18ef81f6f1 | ||
|
|
3cd8191aa4 | ||
|
|
ff0d338899 | ||
|
|
e44114b5e5 | ||
|
|
7587fea6d5 | ||
|
|
a105e85a5a | ||
|
|
3224feeb9e | ||
|
|
cb98359d35 | ||
|
|
64390bae2b | ||
|
|
94d10b6385 | ||
|
|
08875b07ec | ||
|
|
c20b8c665a | ||
|
|
e6493df881 | ||
|
|
1ed317d0cb | ||
|
|
df63e74116 | ||
|
|
910bf4c72e | ||
|
|
0ac1f13043 | ||
|
|
76de8f11f2 | ||
|
|
711e85cb6e | ||
|
|
f41709bd12 | ||
|
|
59955cd29a | ||
|
|
e438e4572f | ||
|
|
f445c2b310 | ||
|
|
5dd52808b3 | ||
|
|
a85d2c8559 | ||
|
|
427f52b5a4 | ||
|
|
c1b828a58e | ||
|
|
89bc2a9293 | ||
|
|
8c9084cc7a | ||
|
|
00f3941ca6 | ||
|
|
74915f6840 | ||
|
|
3ac0eb2128 | ||
|
|
c5efa71875 | ||
|
|
6460fe33d6 | ||
|
|
6b81eabf07 | ||
|
|
d63e3db216 | ||
|
|
7e873161bc | ||
|
|
fe5f24ef44 | ||
|
|
21a26a5ebb | ||
|
|
4608390cfd | ||
|
|
a73d3ec83e | ||
|
|
1b40aa0ded | ||
|
|
de673ea5d1 | ||
|
|
4c3ea2fd65 | ||
|
|
f2000ffe56 | ||
|
|
a0de1e3c72 | ||
|
|
b825e0fef4 | ||
|
|
5b21906894 | ||
|
|
46b2b4c3ab | ||
|
|
ff11a91190 | ||
|
|
eb79eef2a2 | ||
|
|
e8a4496199 | ||
|
|
04ab4e23db | ||
|
|
e36abfa9a6 | ||
|
|
358381ddc2 | ||
|
|
1fbfbc4204 | ||
|
|
95703c1505 | ||
|
|
0225b93c72 | ||
|
|
3027606524 | ||
|
|
92ddf023ec | ||
|
|
5a50e9b301 | ||
|
|
0f10a39f91 | ||
|
|
55a871cb3c | ||
|
|
bd78dbb95d | ||
|
|
274595a533 | ||
|
|
5690d91d59 | ||
|
|
1cdb3d1ef7 | ||
|
|
048c0a7994 | ||
|
|
8412e0f17f | ||
|
|
e04bfcc96a | ||
|
|
f6f1842863 | ||
|
|
5e2fa7a8f0 | ||
|
|
d0dbc75587 | ||
|
|
3886617b11 | ||
|
|
e4ade39db7 | ||
|
|
1f6826e045 | ||
|
|
6d236477ac | ||
|
|
32817829d8 | ||
|
|
afbd1157c0 | ||
|
|
8de2c0387c | ||
|
|
267693668d | ||
|
|
7475dcfb21 | ||
|
|
6d43665e59 | ||
|
|
a98c36aea3 | ||
|
|
fdf8e88e44 | ||
|
|
f2b7a9cc67 | ||
|
|
ecd8ca0940 | ||
|
|
dfe88b30e8 | ||
|
|
a13056bd1b | ||
|
|
ccc9faa892 | ||
|
|
fe041bc0c0 | ||
|
|
1bf1387a05 | ||
|
|
958a8ad5a7 | ||
|
|
9c921896fd | ||
|
|
b0cf59b0f9 | ||
|
|
ded137a197 | ||
|
|
9a22d4a971 | ||
|
|
9e42fd6ffd | ||
|
|
a101bd479a | ||
|
|
a597d23c95 | ||
|
|
9591ba3dc4 | ||
|
|
1f9fbf7115 | ||
|
|
1286dd30b3 | ||
|
|
8498d7abc9 | ||
|
|
41e3e262fb | ||
|
|
c119df1553 | ||
|
|
c02ba84864 | ||
|
|
fabbc659ad | ||
|
|
4241758354 | ||
|
|
59f1ce7d31 | ||
|
|
2e7b67d991 | ||
|
|
62a79aaf4b | ||
|
|
1b97a2d488 | ||
|
|
4e414e289d | ||
|
|
4ed4903272 | ||
|
|
8075d3c63c | ||
|
|
d6658d16f9 | ||
|
|
6b539b009b | ||
|
|
50e6767259 | ||
|
|
bc06db03a2 | ||
|
|
b8284bfddf | ||
|
|
c84322c142 | ||
|
|
70055c712b | ||
|
|
fd0b1633de | ||
|
|
aa91c9ff86 | ||
|
|
2486bfa146 | ||
|
|
4f678827f3 | ||
|
|
5018a2dfde | ||
|
|
126fda6bc0 | ||
|
|
a3fb77565a | ||
|
|
064fb1bfd9 | ||
|
|
c008d2e046 | ||
|
|
7cfdad3563 | ||
|
|
ed5d5986ff | ||
|
|
e6c2e34b78 | ||
|
|
5b86d892c5 | ||
|
|
459906ef25 | ||
|
|
1e064fb771 | ||
|
|
5254dc1984 | ||
|
|
f245e49b20 | ||
|
|
f25e54eefe | ||
|
|
bfa713fd7e | ||
|
|
73cad941b0 | ||
|
|
7d0668279e | ||
|
|
9b2a9f5731 | ||
|
|
9d130ee935 | ||
|
|
2758e5f050 | ||
|
|
426545a429 | ||
|
|
75f85cc4c8 | ||
|
|
530bea6f45 | ||
|
|
a12d733e02 | ||
|
|
2e332f791b | ||
|
|
deedc9eabd | ||
|
|
5c5e563d42 | ||
|
|
a777777ac6 | ||
|
|
0dee7cf84f | ||
|
|
1b81251c93 | ||
|
|
9ce1bf9bfb | ||
|
|
94c2cd0582 | ||
|
|
c1af1cf2cd | ||
|
|
ab964416ae | ||
|
|
bfa6197ee4 | ||
|
|
3586fc4e6d | ||
|
|
5ddb74e70c | ||
|
|
2d52ebad40 | ||
|
|
02ae6f3bd4 | ||
|
|
7ff8299e6b | ||
|
|
ad30bf6ee8 | ||
|
|
8ec6c23250 | ||
|
|
a18a62f80e | ||
|
|
a0ba1b63c3 | ||
|
|
e7ed85405b | ||
|
|
118ad67369 | ||
|
|
76d69005aa | ||
|
|
d28e2bf5fb | ||
|
|
9681c7e2ee | ||
|
|
fa7624e436 | ||
|
|
4aaa1b6674 | ||
|
|
e8d636dcfd | ||
|
|
124683f8c1 | ||
|
|
dd93b9f36b | ||
|
|
b16634ade9 | ||
|
|
ba703ac377 | ||
|
|
37c0dbf567 | ||
|
|
47705f85ef | ||
|
|
b78389f113 | ||
|
|
26fa94abfb | ||
|
|
6d39301ca8 | ||
|
|
429d68859c | ||
|
|
d3b446685f | ||
|
|
03bb550003 | ||
|
|
97a73a385a | ||
|
|
3ddea2ef8b | ||
|
|
9330f2fb0d | ||
|
|
35157d87fe | ||
|
|
36ae38391b | ||
|
|
deec5a096e | ||
|
|
7b117d938f | ||
|
|
e04212f9e9 | ||
|
|
62e259e3e6 | ||
|
|
dab2bb466f | ||
|
|
0628fb2e41 | ||
|
|
fefe124b87 | ||
|
|
d5d3e14d96 | ||
|
|
785b20d903 | ||
|
|
016cd6fef0 | ||
|
|
ff819b3f9f | ||
|
|
667a388fe1 | ||
|
|
d9d25bb108 | ||
|
|
18eba3dbee | ||
|
|
b6699c8aef | ||
|
|
d6681885e7 | ||
|
|
870e84b539 | ||
|
|
c13a3e93b2 | ||
|
|
72cd3b592e | ||
|
|
4d5c942464 | ||
|
|
20911697cc | ||
|
|
08e1e22dad | ||
|
|
885c63b147 | ||
|
|
7651596617 | ||
|
|
fd1edf9651 | ||
|
|
9414b87e27 | ||
|
|
942104aea2 | ||
|
|
3ff0f6d399 | ||
|
|
d07063b21b | ||
|
|
0be0c5bf3f | ||
|
|
f82665006c | ||
|
|
27e02c53b8 | ||
|
|
17dcdfcb06 | ||
|
|
6ba52156b8 | ||
|
|
dbb6e4d37f | ||
|
|
586c9d054c | ||
|
|
8643ac5661 | ||
|
|
79806322b1 | ||
|
|
13e80edb03 | ||
|
|
4b22bcebdd | ||
|
|
b907ae9705 | ||
|
|
de1d1f8cad | ||
|
|
1cb1f48cdc | ||
|
|
02e48ad8da | ||
|
|
8e6eeabb48 | ||
|
|
c22a0cea99 | ||
|
|
d13c4a5531 | ||
|
|
4b950c8a83 | ||
|
|
cf344b1809 | ||
|
|
aaba9e8d42 | ||
|
|
dbcdb24da6 | ||
|
|
083c395f10 | ||
|
|
8ef45a7fb9 | ||
|
|
5872879c9a | ||
|
|
f6a2a66386 | ||
|
|
d71181e65b | ||
|
|
bea8ddb330 | ||
|
|
0906e883c5 | ||
|
|
e7694afeba | ||
|
|
9d69bd6782 | ||
|
|
e6a387b8b2 | ||
|
|
b471c49cbf | ||
|
|
79abb0fd84 | ||
|
|
69bcd07c01 | ||
|
|
4a07c4f985 | ||
|
|
443c002c31 | ||
|
|
fcfb225410 | ||
|
|
ed03c17c22 | ||
|
|
1d6a5a4442 | ||
|
|
f415ca543b | ||
|
|
f7beb4db0e | ||
|
|
3fda54ca41 | ||
|
|
5ba352028d | ||
|
|
0fdb0a9e05 | ||
|
|
93fab04d7c | ||
|
|
27649b33c0 | ||
|
|
591fe567ec | ||
|
|
eb67c7edff | ||
|
|
03a915a915 | ||
|
|
6db850dd4d | ||
|
|
85a428dd92 | ||
|
|
24cf4896aa | ||
|
|
f9fcd4ff3a | ||
|
|
54db96a03e | ||
|
|
9263f9f950 | ||
|
|
04f75efa11 | ||
|
|
a6e900e52a | ||
|
|
d48d3b9ff4 | ||
|
|
8888a007f2 | ||
|
|
e6b823d89e | ||
|
|
7327f24c47 | ||
|
|
a3a381aad1 | ||
|
|
2b046dfe18 | ||
|
|
9da9ef6939 | ||
|
|
18b439bad6 | ||
|
|
0f91d3b6e1 | ||
|
|
edad05b63b | ||
|
|
54d7cbb121 | ||
|
|
3c48eacc98 | ||
|
|
7964e731e4 | ||
|
|
8ffaaea28d | ||
|
|
8585500e7c | ||
|
|
8a0725276b | ||
|
|
a0f41b03ba | ||
|
|
95743508c8 | ||
|
|
3da5be3602 | ||
|
|
f6e460d97a | ||
|
|
be20fb0dbb | ||
|
|
e40b1f9c5b | ||
|
|
7a1b51a1fd | ||
|
|
077fae2147 | ||
|
|
d825e6af7d | ||
|
|
6dc5894b9a | ||
|
|
12925c515a | ||
|
|
8c64221f88 |
No files matched your search
@@ -0,0 +1 @@
|
||||
* @ajhollid @mohicody @Br0wnHammer @Owaiseimdad @karenvicent @shanikauwu1
|
||||
@@ -20,7 +20,8 @@ const { t } = useTranslation();
|
||||
```
|
||||
- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
|
||||
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
|
||||
- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I have no hardcoded dimensions.
|
||||
- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions.
|
||||
- [ ] My PR is granular and targeted to one specific feature.
|
||||
- [ ] I ran `npm run format` in server and client directories, which automatically formats your code.
|
||||
- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
|
||||
|
||||
@@ -1,45 +1,60 @@
|
||||
import axios from 'axios';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { URLSearchParams } from 'url';
|
||||
import axios from "axios";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { URLSearchParams } from "url";
|
||||
|
||||
// POEditor API information
|
||||
const API_TOKEN = process.env.POEDITOR_API;
|
||||
const PROJECT_ID = process.env.POEDITOR_PROJECT_ID;
|
||||
const LANGUAGES = (process.env.LANGUAGES || 'tr,en').split(',');
|
||||
const EXPORT_FORMAT = process.env.EXPORT_FORMAT || 'key_value_json';
|
||||
const LANGUAGES = (process.env.LANGUAGES || "tr,en").split(",");
|
||||
const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json";
|
||||
|
||||
// POEditor API endpoint
|
||||
const API_URL = 'https://api.poeditor.com/v2';
|
||||
const API_URL = "https://api.poeditor.com/v2";
|
||||
|
||||
function normalizeLanguageCode(language) {
|
||||
if (language.includes("-")) {
|
||||
const [base, region] = language.split("-");
|
||||
return `${base}-${region.toUpperCase()}`;
|
||||
}
|
||||
return language;
|
||||
}
|
||||
|
||||
// Function to download translations
|
||||
async function downloadTranslations() {
|
||||
try {
|
||||
console.log('Downloading translations from POEditor...');
|
||||
console.log("Downloading translations from POEditor...");
|
||||
console.log(`Using export format: ${EXPORT_FORMAT}`);
|
||||
|
||||
for (const language of LANGUAGES) {
|
||||
console.log(`Downloading translations for ${language} language...`);
|
||||
|
||||
// Get export URL from POEditor
|
||||
const exportResponse = await axios.post(`${API_URL}/projects/export`,
|
||||
const exportResponse = await axios.post(
|
||||
`${API_URL}/projects/export`,
|
||||
new URLSearchParams({
|
||||
api_token: API_TOKEN,
|
||||
id: PROJECT_ID,
|
||||
language: language,
|
||||
type: EXPORT_FORMAT
|
||||
type: EXPORT_FORMAT,
|
||||
})
|
||||
);
|
||||
|
||||
if (exportResponse.data.response.status !== 'success') {
|
||||
throw new Error(`Failed to get export URL for ${language} language: ${JSON.stringify(exportResponse.data)}`);
|
||||
if (exportResponse.data.response.status !== "success") {
|
||||
throw new Error(
|
||||
`Failed to get export URL for ${language} language: ${JSON.stringify(
|
||||
exportResponse.data
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const fileUrl = exportResponse.data.result.url;
|
||||
console.log(`Export URL obtained for ${language}`);
|
||||
|
||||
// Download translation file
|
||||
const downloadResponse = await axios.get(fileUrl, { responseType: 'json' });
|
||||
const downloadResponse = await axios.get(fileUrl, {
|
||||
responseType: "json",
|
||||
});
|
||||
const translations = downloadResponse.data;
|
||||
console.log(`Downloaded translations for ${language}`);
|
||||
|
||||
@@ -48,9 +63,11 @@ async function downloadTranslations() {
|
||||
|
||||
// If data is in array format, convert it to key-value format
|
||||
if (Array.isArray(translations)) {
|
||||
console.log(`Converting array format to key-value format for ${language}`);
|
||||
console.log(
|
||||
`Converting array format to key-value format for ${language}`
|
||||
);
|
||||
formattedTranslations = {};
|
||||
translations.forEach(item => {
|
||||
translations.forEach((item) => {
|
||||
if (item.term && item.definition) {
|
||||
formattedTranslations[item.term] = item.definition;
|
||||
}
|
||||
@@ -58,16 +75,19 @@ async function downloadTranslations() {
|
||||
}
|
||||
|
||||
// Determine the output filename based on language
|
||||
const filename = language === 'en' ? 'gb.json' : `${language}.json`;
|
||||
const outputPath = path.join(process.cwd(), 'temp', filename);
|
||||
const normalizedLanguage = normalizeLanguageCode(language);
|
||||
const filename = `${normalizedLanguage}.json`;
|
||||
const outputPath = path.join(process.cwd(), "temp", filename);
|
||||
await fs.writeJson(outputPath, formattedTranslations, { spaces: 2 });
|
||||
|
||||
console.log(`Translations for ${language} language successfully downloaded and saved as: ${filename}`);
|
||||
console.log(
|
||||
`Translations for ${language} language successfully downloaded and saved as: ${filename}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('All translations successfully downloaded!');
|
||||
console.log("All translations successfully downloaded!");
|
||||
} catch (error) {
|
||||
console.error('An error occurred while downloading translations:', error);
|
||||
console.error("An error occurred while downloading translations:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -76,15 +96,15 @@ async function downloadTranslations() {
|
||||
async function main() {
|
||||
try {
|
||||
// Clean temp folder
|
||||
await fs.emptyDir(path.join(process.cwd(), 'temp'));
|
||||
await fs.emptyDir(path.join(process.cwd(), "temp"));
|
||||
|
||||
// Download translations
|
||||
await downloadTranslations();
|
||||
} catch (error) {
|
||||
console.error('An error occurred during the process:', error);
|
||||
console.error("An error occurred during the process:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run script
|
||||
main();
|
||||
main();
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Format Check (Client & Server)
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
format-client:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install client dependencies
|
||||
working-directory: client
|
||||
run: npm ci
|
||||
|
||||
- name: Check client formatting
|
||||
working-directory: client
|
||||
run: npm run format-check
|
||||
|
||||
format-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install server dependencies
|
||||
working-directory: server
|
||||
run: npm ci
|
||||
|
||||
- name: Check server formatting
|
||||
working-directory: server
|
||||
run: npm run format-check
|
||||
close-pr-if-needed:
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
needs: [format-client, format-server]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: echo "PR_NUMBER=$(jq -r .pull_request.number "$GITHUB_EVENT_PATH")" >> $GITHUB_ENV
|
||||
|
||||
- name: Close PR using GitHub CLI
|
||||
if: |
|
||||
needs.format-client.result == 'failure' ||
|
||||
needs.format-server.result == 'failure'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr close "$PR_NUMBER" --delete-branch --comment "❌ Formatting check failed — PR auto-closed.
|
||||
Please run \`npm run format\` in both `client` and `server` directories and push again."
|
||||
@@ -0,0 +1,159 @@
|
||||
name: Deploy images on release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
docker-build-and-push-client:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract version from tag
|
||||
id: extract_tag
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Client Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} \
|
||||
-f ./docker/dist/client.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Client Docker image
|
||||
run: |
|
||||
docker push ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }}
|
||||
|
||||
docker-build-and-push-server:
|
||||
needs: docker-build-and-push-client
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract version
|
||||
id: extract_tag
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Server Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} \
|
||||
-f ./docker/dist/server.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Server Docker image
|
||||
run: |
|
||||
docker push ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }}
|
||||
|
||||
- name: Build Mongo Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} \
|
||||
-f ./docker/dist/mongoDB.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push MongoDB Docker image
|
||||
run: |
|
||||
docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }}
|
||||
|
||||
- name: Build Redis Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} \
|
||||
-f ./docker/dist/redis.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Redis Docker image
|
||||
run: |
|
||||
docker push ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }}
|
||||
|
||||
docker-build-and-push-server-mono-multiarch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract version
|
||||
id: extract_tag
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push multi-arch Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dist-arm/server.Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:${{ steps.extract_tag.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
|
||||
build-args: |
|
||||
VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }}
|
||||
|
||||
docker-build-and-push-server-mono:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract version
|
||||
id: extract_tag
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Server Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} \
|
||||
-f ./docker/dist-mono/server.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
--build-arg VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }} \
|
||||
.
|
||||
|
||||
- name: Push Server Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }}
|
||||
@@ -0,0 +1,154 @@
|
||||
name: Deploy images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
docker-build-and-push-client:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version
|
||||
id: vars
|
||||
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
- name: Build Client Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-client:latest \
|
||||
-f ./docker/dist/client.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
|
||||
.
|
||||
|
||||
- name: Push Client Docker image
|
||||
run: |
|
||||
docker push ghcr.io/bluewave-labs/checkmate-client:latest
|
||||
|
||||
docker-build-and-push-server:
|
||||
needs: docker-build-and-push-client
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Server Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-backend:latest \
|
||||
-f ./docker/dist/server.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Server Docker image
|
||||
run: |
|
||||
docker push ghcr.io/bluewave-labs/checkmate-backend:latest
|
||||
|
||||
- name: Build Mongo Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-mongo:latest \
|
||||
-f ./docker/dist/mongoDB.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push MongoDB Docker image
|
||||
run: |
|
||||
docker push ghcr.io/bluewave-labs/checkmate-mongo:latest
|
||||
|
||||
- name: Build Redis Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-redis:latest \
|
||||
-f ./docker/dist/redis.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Redis Docker image
|
||||
run: |
|
||||
docker push ghcr.io/bluewave-labs/checkmate-redis:latest
|
||||
|
||||
docker-build-and-push-server-mono-multiarch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version
|
||||
id: vars
|
||||
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push multi-arch Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dist-arm/server.Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
|
||||
build-args: |
|
||||
VITE_APP_VERSION=${{ env.VERSION }}
|
||||
|
||||
docker-build-and-push-server-mono:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version
|
||||
id: vars
|
||||
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build Server Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate-backend-mono:latest \
|
||||
-f ./docker/dist-mono/server.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
|
||||
.
|
||||
|
||||
- name: Push Server Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:latest
|
||||
@@ -1,77 +0,0 @@
|
||||
name: Distribution deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
docker-build-and-push-client:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Client Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:frontend-dist \
|
||||
-f ./docker/dist/client.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Client Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-dist
|
||||
|
||||
docker-build-and-push-server:
|
||||
needs: docker-build-and-push-client
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Server Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:backend-dist \
|
||||
-f ./docker/dist/server.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Server Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist
|
||||
|
||||
- name: Build Mongo Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:mongo-dist \
|
||||
-f ./docker/dist/mongoDB.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push MongoDB Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-dist
|
||||
|
||||
- name: Build Redis Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:redis-dist \
|
||||
-f ./docker/dist/redis.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Redis Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:redis-dist
|
||||
@@ -7,13 +7,12 @@ on:
|
||||
languages:
|
||||
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
|
||||
required: false
|
||||
default: "tr,en"
|
||||
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr"
|
||||
format:
|
||||
description: "Export format (key_value_json or json)"
|
||||
required: false
|
||||
default: "key_value_json"
|
||||
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -93,6 +92,12 @@ jobs:
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Format client code
|
||||
run: |
|
||||
cd client
|
||||
npm ci
|
||||
npm run format
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -18,12 +20,16 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version
|
||||
id: vars
|
||||
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
- name: Build Client Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:frontend-demo \
|
||||
-f ./docker/prod/client.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
|
||||
.
|
||||
|
||||
- name: Push Client Docker image
|
||||
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -18,12 +20,16 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version
|
||||
id: vars
|
||||
run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
|
||||
- name: Build Client Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:frontend-staging \
|
||||
-f ./docker/staging/client.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
--build-arg VITE_APP_VERSION=${{ env.VERSION }} \
|
||||
.
|
||||
|
||||
- name: Push Client Docker image
|
||||
|
||||
+5
-1
@@ -27,6 +27,10 @@ Process. If more revisions are required after the second review we’re looking
|
||||
|
||||
If PRs are small and manageable it is far more likely that a dev will catch bugs during the review process. If our eyes glaze over at line 400 of a 700 line PR since we’ve reached our cognitive limit we’re not going to likely miss bugs in the last 300 lines of code.
|
||||
|
||||
### Bonus Topic: Keep PRs focused
|
||||
### Format your PRs for better readability
|
||||
|
||||
Ensure you execute `npm run format` before submitting your pull requests in both the client and server directories. This command automatically applies our formatting structure, making the code easier to follow and review.
|
||||
|
||||
### Keep PRs focused
|
||||
|
||||
It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a big or work on another feature.
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
|
||||
|
||||

|
||||
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
|
||||
|
||||
This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
|
||||
|
||||
@@ -22,7 +22,24 @@ Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/c
|
||||
|
||||
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
|
||||
|
||||
We **love** what we are building here, and we continuously learn a few things about Reactjs, Nodejs, MongoDB, and Docker while building Checkmate.
|
||||
**If you would like to sponsor a feature, [see this link](https://checkmate.so/sponsored-features).**
|
||||
|
||||
## 📚 Table of contents
|
||||
|
||||
- [📦 Demo](#-demo)
|
||||
- [🔗 User's guide](#-users-guide)
|
||||
- [🛠️ Installation](#️-installation)
|
||||
- [🚀 Deploying Checkmate with Helm](#-deploying-checkmate-with-helm)
|
||||
- [🏁 Translations](#-translations)
|
||||
- [🚀 Performance](#-performance)
|
||||
- [💚 Questions & Ideas](#-questions--ideas)
|
||||
- [🧩 Features](#-features)
|
||||
- [🏗️ Screenshots](#-screenshots)
|
||||
- [🏗️ Tech stack](#-tech-stack)
|
||||
- [🔗 A few links](#a-few-links)
|
||||
- [🤝 Contributing](#-contributing)
|
||||
- [💰 Our sponsors](#-our-sponsors)
|
||||
|
||||
|
||||
## 📦 Demo
|
||||
|
||||
@@ -34,7 +51,10 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/) or [Elestio](https://elest.io/open-source/checkmate) for a one-click Docker deployment. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
||||
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
|
||||
|
||||
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
||||
|
||||
|
||||
## 🏁 Translations
|
||||
|
||||
@@ -56,7 +76,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
|
||||
## 🧩 Features
|
||||
|
||||
- Completely open source, deployable on your servers
|
||||
- Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5)
|
||||
- Website monitoring
|
||||
- Page speed monitoring
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
|
||||
@@ -65,30 +85,34 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
- SSL monitoring
|
||||
- Port monitoring
|
||||
- Incidents at a glance
|
||||
- E-mail notifications
|
||||
- Status pages
|
||||
- E-mail, Webhooks, Discord, Telegram, Slack notifications
|
||||
- Scheduled maintenance
|
||||
- JSON query monitoring
|
||||
- Support for multiple languages
|
||||
|
||||
**Short term roadmap:**
|
||||
**Short term roadmap:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
|
||||
|
||||
- Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131
|
||||
- Translations (i18n) (**in progress**)
|
||||
- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545
|
||||
- JSON query monitoring (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1573
|
||||
- Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546
|
||||
- More configuration options
|
||||
- DNS monitoring
|
||||
- Better notifications
|
||||
- Network monitoring
|
||||
- ..and a few more features
|
||||
|
||||
## 🏗️ Screenshots
|
||||
|
||||
<p>
|
||||
<img width="2714" alt="server" src="https://github.com/user-attachments/assets/f7cb272a-69a6-48c5-93b0-249ecf20ecc6" />
|
||||
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
|
||||
</p>
|
||||
<p>
|
||||
<img width="2714" alt="uptime" src="https://github.com/user-attachments/assets/98ddc6c0-3384-47fd-96ce-7e53e6b688ac" />
|
||||
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
|
||||
</p>
|
||||
<p>
|
||||
<img width="2714" alt="page speed" src="https://github.com/user-attachments/assets/b5589f79-da30-4239-9846-1f8bb2637ff9" />
|
||||
</p><img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
|
||||
</p>
|
||||
<p>
|
||||
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
## 🏗️ Tech stack
|
||||
|
||||
@@ -108,9 +132,9 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github/com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
|
||||
We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
|
||||
|
||||
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 5000+ stars and attracted 60 contributors from around the globe.
|
||||
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 6000+ stars and attracted 80+ contributors from around the globe.
|
||||
|
||||
Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us!
|
||||
|
||||
@@ -128,11 +152,13 @@ Here's how you can contribute:
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
</a>
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
|
||||
|
||||
## 💰 Our sponsors
|
||||
|
||||
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
|
||||
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
|
||||
|
||||
Also check other developer and contributor-friendly projects of BlueWave:
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: checkmate-chart
|
||||
description: A Helm chart for Checkmate App
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "2.3"
|
||||
@@ -0,0 +1,39 @@
|
||||
# Kubernetes Installation Guide for Checkmate
|
||||
|
||||
This guide walks you through deploying Checkmate on your Kubernetes cluster using Helm.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running Kubernetes cluster
|
||||
- Helm CLI installed and configured
|
||||
- `kubectl` configured to access your cluster
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Clone the repo and navigate to the Helm chart
|
||||
|
||||
```bash
|
||||
git clone https://github.com/bluewave-labs/checkmate.git
|
||||
cd checkmate/charts/helm/checkmate
|
||||
```
|
||||
|
||||
### 2. Customize values.yaml
|
||||
Edit `values.yaml` to update:
|
||||
- `client.ingress.host` and `server.ingress.host` with your domain names
|
||||
- `server.protocol` (usually http or https)
|
||||
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
|
||||
|
||||
### 3. Deploy the Helm chart
|
||||
```bash
|
||||
helm install checkmate ./charts/helm/checkmate
|
||||
```
|
||||
This will deploy the client, server, MongoDB, and Redis components.
|
||||
|
||||
### 4. Verify the deployment
|
||||
Check pods and services:
|
||||
```bash
|
||||
kubectl get pods
|
||||
kubectl get svc
|
||||
```
|
||||
|
||||
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
|
||||
@@ -0,0 +1,12 @@
|
||||
{{- if eq .Values.client.ingress.host "change_me" }}
|
||||
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.server.ingress.host "change_me" }}
|
||||
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $protocol := .Values.server.protocol }}
|
||||
{{- if not (or (eq $protocol "http") (eq $protocol "https")) }}
|
||||
{{- fail "server.protocol must be either 'http' or 'https'" }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: client
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: client
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: client
|
||||
spec:
|
||||
containers:
|
||||
- name: client
|
||||
image: {{ .Values.client.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.client.port }}
|
||||
env:
|
||||
- name: UPTIME_APP_API_BASE_URL
|
||||
value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1"
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- if .Values.client.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: client-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.client.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: client
|
||||
port:
|
||||
number: {{ .Values.client.port }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: client
|
||||
spec:
|
||||
selector:
|
||||
app: client
|
||||
ports:
|
||||
- port: {{ .Values.client.port }}
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mongodb
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mongodb
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mongodb
|
||||
spec:
|
||||
containers:
|
||||
- name: mongodb
|
||||
image: {{ .Values.mongodb.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.mongodb.port }}
|
||||
command: ["mongod", "--quiet", "--bind_ip_all"]
|
||||
volumeMounts:
|
||||
- name: mongodb-data
|
||||
mountPath: /data/db
|
||||
volumes:
|
||||
- name: mongodb-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mongodb-pvc
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mongodb-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.mongodbSize }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mongodb
|
||||
spec:
|
||||
selector:
|
||||
app: mongodb
|
||||
ports:
|
||||
- port: {{ .Values.mongodb.port }}
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: {{ .Values.redis.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.redis.port }}
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: redis-data
|
||||
persistentVolumeClaim:
|
||||
claimName: redis-pvc
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: redis-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.redisSize }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: {{ .Values.redis.port }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- $secrets := .Values.secrets }}
|
||||
|
||||
{{- if or (not $secrets.JWT_SECRET) (eq $secrets.JWT_SECRET "change_me") }}
|
||||
{{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if or (not $secrets.REFRESH_TOKEN_SECRET) (eq $secrets.REFRESH_TOKEN_SECRET "change_me") }}
|
||||
{{- fail "secrets.REFRESH_TOKEN_SECRET must be overridden and cannot be 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: checkmate-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- range $key, $value := := $secrets }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,30 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: server
|
||||
spec:
|
||||
containers:
|
||||
- name: server
|
||||
image: {{ .Values.server.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.server.port }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets
|
||||
volumeMounts:
|
||||
- name: docker-sock
|
||||
mountPath: /var/run/docker.sock
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
hostPath:
|
||||
path: /var/run/docker.sock
|
||||
type: Socket
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- if .Values.server.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: server-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.server.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: server
|
||||
port:
|
||||
number: {{ .Values.server.port }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: server
|
||||
spec:
|
||||
selector:
|
||||
app: server
|
||||
ports:
|
||||
- port: {{ .Values.server.port }}
|
||||
@@ -0,0 +1,41 @@
|
||||
client:
|
||||
image: ghcr.io/bluewave-labs/checkmate-client:v2.3
|
||||
port: 80
|
||||
ingress:
|
||||
enabled: true
|
||||
host: change_me
|
||||
|
||||
server:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend:v2.3
|
||||
port: 52345
|
||||
protocol: change_me
|
||||
ingress:
|
||||
enabled: true
|
||||
host: change_me
|
||||
|
||||
redis:
|
||||
image: redis:7.2
|
||||
port: 6379
|
||||
|
||||
mongodb:
|
||||
image: mongo:7.0
|
||||
port: 27017
|
||||
|
||||
secrets:
|
||||
JWT_SECRET: change_me
|
||||
REFRESH_TOKEN_SECRET: change_me
|
||||
SYSTEM_EMAIL_ADDRESS: test@example.com
|
||||
SYSTEM_EMAIL_PASSWORD: change_me
|
||||
SYSTEM_EMAIL_HOST: smtp.example.com
|
||||
SYSTEM_EMAIL_PORT: "587"
|
||||
PAGESPEED_API_KEY: change_me
|
||||
DB_CONNECTION_STRING: mongodb://mongodb:27017/uptime_db
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: "6379"
|
||||
DB_TYPE: MongoDB
|
||||
TOKEN_TTL: 99d
|
||||
REFRESH_TOKEN_TTL: 99d
|
||||
|
||||
persistence:
|
||||
mongodbSize: 5Gi
|
||||
redisSize: 1Gi
|
||||
@@ -16,4 +16,8 @@ module.exports = {
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"react/no-unescaped-entities": "off",
|
||||
},
|
||||
globals: {
|
||||
__APP_VERSION__: "readonly",
|
||||
process: "readonly",
|
||||
},
|
||||
};
|
||||
@@ -1,2 +1 @@
|
||||
|
||||
This directory contains the client side (frontend) of Checkmate.
|
||||
+20
-13
@@ -1,17 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
href="./checkmate_favicon.svg"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="./checkmate_favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Checkmate</title>
|
||||
</head>
|
||||
|
||||
<title>Checkmate</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/main.jsx"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1527
-1064
File diff suppressed because it is too large.
Load diff
+9
-11
@@ -8,33 +8,28 @@
|
||||
"build": "vite build",
|
||||
"build-dev": "vite build --mode development",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.29.0",
|
||||
"@mui/x-date-pickers": "7.29.0",
|
||||
"@mui/icons-material": "6.4.11",
|
||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
||||
"@mui/material": "6.4.11",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-date-pickers": "7.29.0",
|
||||
"@reduxjs/toolkit": "2.7.0",
|
||||
"axios": "^1.7.4",
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"i18next": "^24.2.2",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"joi": "17.13.3",
|
||||
"maplibre-gl": "5.3.1",
|
||||
"mui-color-input": "^6.0.0",
|
||||
"react": "18.3.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-icons": "5.5.0",
|
||||
@@ -63,6 +58,9 @@
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "^5.2.0"
|
||||
"vite": "6.3.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-arm64-musl": "4.41.0"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"labels": ["dependencies"]
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"labels": ["dependencies"]
|
||||
}
|
||||
+5
-11
@@ -10,6 +10,7 @@ import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
import { Routes } from "./Routes";
|
||||
import WalletProvider from "./Components/WalletProvider";
|
||||
import AppLayout from "./Components/Layouts/AppLayout";
|
||||
|
||||
function App() {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
@@ -27,17 +28,10 @@ function App() {
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
<WalletProvider>
|
||||
<CssBaseline />
|
||||
<GlobalStyles
|
||||
styles={({ palette }) => {
|
||||
return {
|
||||
body: {
|
||||
backgroundImage: `radial-gradient(circle, ${palette.gradient.color1}, ${palette.gradient.color2}, ${palette.gradient.color3}, ${palette.gradient.color4}, ${palette.gradient.color5})`,
|
||||
color: palette.primary.contrastText,
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<Routes />
|
||||
|
||||
<AppLayout>
|
||||
<Routes />
|
||||
</AppLayout>
|
||||
<ToastContainer />
|
||||
</WalletProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
// Components
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Settings from "../../assets/icons/settings-bold.svg?react";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { logger } from "../../Utils/Logger";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import {
|
||||
deleteUptimeMonitor,
|
||||
pauseUptimeMonitor,
|
||||
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Settings from "../../assets/icons/settings-bold.svg?react";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
import { usePauseMonitor, useDeleteMonitor } from "../../Hooks/monitorHooks";
|
||||
|
||||
const ActionsMenu = ({
|
||||
monitor,
|
||||
@@ -23,38 +24,27 @@ const ActionsMenu = ({
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [actions, setActions] = useState({});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const [pauseMonitor, isPausing, error] = usePauseMonitor();
|
||||
const [deleteMonitor, isDeleting] = useDeleteMonitor();
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let monitor = { _id: actions.id };
|
||||
const action = await dispatch(deleteUptimeMonitor({ monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
setIsOpen(false); // close modal
|
||||
updateRowCallback();
|
||||
createToast({ body: "Monitor deleted successfully." });
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
await deleteMonitor({ monitor });
|
||||
updateRowCallback();
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const action = await dispatch(pauseUptimeMonitor({ monitorId: monitor._id }));
|
||||
if (pauseUptimeMonitor.fulfilled.match(action)) {
|
||||
const state = action?.payload?.data.isActive === false ? "resumed" : "paused";
|
||||
createToast({ body: `Monitor ${state} successfully.` });
|
||||
pauseCallback();
|
||||
} else {
|
||||
throw new Error(action?.error?.message ?? "Failed to pause monitor.");
|
||||
}
|
||||
await pauseMonitor({ monitorId: monitor._id });
|
||||
pauseCallback();
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor:", monitor._id, error);
|
||||
createToast({ body: "Failed to pause monitor." });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,7 +200,7 @@ const ActionsMenu = ({
|
||||
e.stopPropagation();
|
||||
handleRemove(e);
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
isLoading={isDeleting}
|
||||
modelTitle="modal-delete-monitor"
|
||||
modelDescription="delete-monitor-confirmation"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@@ -3,28 +3,28 @@ import Button from "@mui/material/Button";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
const RoundGradientButton = styled(Button)(({ theme }) => ({
|
||||
position: "relative",
|
||||
border: "5px solid transparent",
|
||||
backgroundClip: "padding-box",
|
||||
borderRadius: 30,
|
||||
fontSize: "1.2rem",
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
position: "relative",
|
||||
border: "5px solid transparent",
|
||||
backgroundClip: "padding-box",
|
||||
borderRadius: 30,
|
||||
fontSize: "1.2rem",
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
|
||||
"&:after": {
|
||||
position: "absolute",
|
||||
top: -3,
|
||||
left: -3,
|
||||
right: -3,
|
||||
bottom: -3,
|
||||
background:
|
||||
theme.palette.mode === "dark"
|
||||
? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)"
|
||||
: "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)",
|
||||
content: '""',
|
||||
zIndex: -1,
|
||||
borderRadius: 30,
|
||||
},
|
||||
"&:after": {
|
||||
position: "absolute",
|
||||
top: -3,
|
||||
left: -3,
|
||||
right: -3,
|
||||
bottom: -3,
|
||||
background:
|
||||
theme.palette.mode === "dark"
|
||||
? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)"
|
||||
: "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)",
|
||||
content: '""',
|
||||
zIndex: -1,
|
||||
borderRadius: 30,
|
||||
},
|
||||
}));
|
||||
|
||||
export default RoundGradientButton;
|
||||
@@ -31,78 +31,76 @@ import IconBox from "../../IconBox";
|
||||
*/
|
||||
|
||||
const EmptyView = ({
|
||||
icon,
|
||||
header,
|
||||
message = "No Data",
|
||||
headingLevel = "h2",
|
||||
justifyContent = "flex-start",
|
||||
height = "100%"
|
||||
icon,
|
||||
header,
|
||||
message = "No Data",
|
||||
headingLevel = "h2",
|
||||
justifyContent = "flex-start",
|
||||
height = "100%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
direction="row"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
direction="row"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: 2,
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
flex={1}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
padding: theme.spacing(8),
|
||||
justifyContent,
|
||||
gap: theme.spacing(8),
|
||||
height,
|
||||
"& h2": {
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
},
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: 2,
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
flex={1}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
padding: theme.spacing(8),
|
||||
justifyContent,
|
||||
gap: theme.spacing(8),
|
||||
height,
|
||||
"& h2": {
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
},
|
||||
|
||||
"& tspan, & text": {
|
||||
fill: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
alignSelf="flex-start"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
{icon && <IconBox>{icon}</IconBox>}
|
||||
{header && <Typography component="h2">{header}</Typography>}
|
||||
</Stack>
|
||||
<Stack
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography component={headingLevel}>
|
||||
{message}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
"& tspan, & text": {
|
||||
fill: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
alignSelf="flex-start"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
{icon && <IconBox>{icon}</IconBox>}
|
||||
{header && <Typography component="h2">{header}</Typography>}
|
||||
</Stack>
|
||||
<Stack
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography component={headingLevel}>{message}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
EmptyView.propTypes = {
|
||||
message: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
header: PropTypes.string,
|
||||
headingLevel: PropTypes.oneOf(['h1', 'h2', 'h3']),
|
||||
justifyContent: PropTypes.string,
|
||||
height: PropTypes.string
|
||||
message: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
header: PropTypes.string,
|
||||
headingLevel: PropTypes.oneOf(["h1", "h2", "h3"]),
|
||||
justifyContent: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
};
|
||||
|
||||
export default EmptyView;
|
||||
@@ -18,7 +18,13 @@ const ChartBox = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
if (isEmpty) {
|
||||
return <EmptyView icon={icon} header={header} message={noDataMessage} />;
|
||||
return (
|
||||
<EmptyView
|
||||
icon={icon}
|
||||
header={header}
|
||||
message={noDataMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack
|
||||
@@ -37,7 +43,6 @@ const ChartBox = ({
|
||||
>
|
||||
<Stack
|
||||
flex={1}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
padding: theme.spacing(8),
|
||||
justifyContent,
|
||||
@@ -95,5 +100,5 @@ ChartBox.propTypes = {
|
||||
header: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
noDataMessage: PropTypes.string,
|
||||
isEmpty: PropTypes.bool
|
||||
isEmpty: PropTypes.bool,
|
||||
};
|
||||
@@ -47,9 +47,6 @@ const AppAppBar = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Debugging: Log the current theme mode
|
||||
console.log("Current theme mode:", mode);
|
||||
|
||||
const logoSrc =
|
||||
mode === "light" ? "/images/prism-black.png" : "/images/prism-white.png";
|
||||
|
||||
|
||||
@@ -1,214 +1,255 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Container from '@mui/material/Container';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from 'react-icons/fa';
|
||||
|
||||
import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Container from "@mui/material/Container";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Link from "@mui/material/Link";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from "react-icons/fa";
|
||||
|
||||
function Copyright() {
|
||||
return (
|
||||
<Typography sx={{ color: 'text.secondary', mt: 1 }}>
|
||||
{'Copyright © '}
|
||||
<Link color="text.secondary" href="https://prism.uprock.com/">
|
||||
UpRock
|
||||
</Link>
|
||||
|
||||
{new Date().getFullYear()}
|
||||
</Typography>
|
||||
);
|
||||
return (
|
||||
<Typography sx={{ color: "text.secondary", mt: 1 }}>
|
||||
{"Copyright © "}
|
||||
<Link
|
||||
color="text.secondary"
|
||||
href="https://prism.uprock.com/"
|
||||
>
|
||||
UpRock
|
||||
</Link>
|
||||
|
||||
{new Date().getFullYear()}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: { xs: 4, sm: 8 },
|
||||
py: { xs: 24, sm: 24 },
|
||||
px: { xs: 12, sm: 12 },
|
||||
textAlign: { sm: 'center', md: 'left' },
|
||||
}}
|
||||
>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
pt: { xs: 4, sm: 8 },
|
||||
width: '100%',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Link color="text.secondary" href="https://uprock.com/privacy-policy">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Typography sx={{ display: 'inline', mx: 0.5, opacity: 0.5 }}>
|
||||
•
|
||||
</Typography>
|
||||
<Link color="text.secondary" href="https://uprock.com/terms-of-use">
|
||||
Terms of Service
|
||||
</Link>
|
||||
<Copyright />
|
||||
</div>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
useFlexGap
|
||||
sx={{ justifyContent: 'left', color: 'text.secondary' }}
|
||||
>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="mailto:prism@uprock.com?subject=Interested%20in%20UpRock%20Prism"
|
||||
aria-label="Contact Us"
|
||||
sx={{ alignSelf: 'center' }}
|
||||
>
|
||||
<FaEnvelope />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="https://facebook.com/uprockcom"
|
||||
aria-label="Facebook"
|
||||
sx={{ alignSelf: 'center' }}
|
||||
>
|
||||
<FaFacebook />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="https://x.com/uprockcom"
|
||||
aria-label="X"
|
||||
sx={{ alignSelf: 'center' }}
|
||||
>
|
||||
<FaTwitter />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="https://www.linkedin.com/company/uprock/"
|
||||
aria-label="LinkedIn"
|
||||
sx={{ alignSelf: 'center' }}
|
||||
>
|
||||
<FaLinkedin />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="https://github.com/uprockcom"
|
||||
aria-label="GitHub"
|
||||
sx={{ alignSelf: 'center' }}
|
||||
>
|
||||
<FaGithub />
|
||||
</IconButton>
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: { xs: 4, sm: 8 },
|
||||
py: { xs: 24, sm: 24 },
|
||||
px: { xs: 12, sm: 12 },
|
||||
textAlign: { sm: "center", md: "left" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
pt: { xs: 4, sm: 8 },
|
||||
width: "100%",
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Link
|
||||
color="text.secondary"
|
||||
href="https://uprock.com/privacy-policy"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Typography sx={{ display: "inline", mx: 0.5, opacity: 0.5 }}>
|
||||
•
|
||||
</Typography>
|
||||
<Link
|
||||
color="text.secondary"
|
||||
href="https://uprock.com/terms-of-use"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
<Copyright />
|
||||
</div>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
useFlexGap
|
||||
sx={{ justifyContent: "left", color: "text.secondary" }}
|
||||
>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="mailto:prism@uprock.com?subject=Interested%20in%20UpRock%20Prism"
|
||||
aria-label="Contact Us"
|
||||
sx={{ alignSelf: "center" }}
|
||||
>
|
||||
<FaEnvelope />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="https://facebook.com/uprockcom"
|
||||
aria-label="Facebook"
|
||||
sx={{ alignSelf: "center" }}
|
||||
>
|
||||
<FaFacebook />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="https://x.com/uprockcom"
|
||||
aria-label="X"
|
||||
sx={{ alignSelf: "center" }}
|
||||
>
|
||||
<FaTwitter />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="https://www.linkedin.com/company/uprock/"
|
||||
aria-label="LinkedIn"
|
||||
sx={{ alignSelf: "center" }}
|
||||
>
|
||||
<FaLinkedin />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
href="https://github.com/uprockcom"
|
||||
aria-label="GitHub"
|
||||
sx={{ alignSelf: "center" }}
|
||||
>
|
||||
<FaGithub />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
mt: 4,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2" sx={{ color: 'text.secondary' }}>
|
||||
Made with ❤️ by
|
||||
<Link href="https://uprock.com" color="inherit" sx={{ mx: 0.5 }}>
|
||||
UpRock
|
||||
</Link>
|
||||
&
|
||||
<Link href="https://bluewavelabs.ca" color="inherit" sx={{ mx: 0.5 }}>
|
||||
Bluewave Labs
|
||||
</Link>
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2" sx={{ color: 'text.secondary', mr: 1 }}>
|
||||
Built on
|
||||
</Typography>
|
||||
<svg
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 397.7 311.7"
|
||||
xmlSpace="preserve"
|
||||
width="15"
|
||||
height="15"
|
||||
>
|
||||
<style type="text/css">
|
||||
{`.st0{fill:url(#SVGID_1_);}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
mt: 4,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{ color: "text.secondary" }}
|
||||
>
|
||||
Made with ❤️ by
|
||||
<Link
|
||||
href="https://uprock.com"
|
||||
color="inherit"
|
||||
sx={{ mx: 0.5 }}
|
||||
>
|
||||
UpRock
|
||||
</Link>
|
||||
&
|
||||
<Link
|
||||
href="https://bluewavelabs.ca"
|
||||
color="inherit"
|
||||
sx={{ mx: 0.5 }}
|
||||
>
|
||||
Bluewave Labs
|
||||
</Link>
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{ color: "text.secondary", mr: 1 }}
|
||||
>
|
||||
Built on
|
||||
</Typography>
|
||||
<svg
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 397.7 311.7"
|
||||
xmlSpace="preserve"
|
||||
width="15"
|
||||
height="15"
|
||||
>
|
||||
<style type="text/css">
|
||||
{`.st0{fill:url(#SVGID_1_);}
|
||||
.st1{fill:url(#SVGID_2_);}
|
||||
.st2{fill:url(#SVGID_3_);}`}
|
||||
</style>
|
||||
<linearGradient
|
||||
id="SVGID_1_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="360.8791"
|
||||
y1="351.4553"
|
||||
x2="141.213"
|
||||
y2="-69.2936"
|
||||
gradientTransform="matrix(1 0 0 -1 0 314)"
|
||||
>
|
||||
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} />
|
||||
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} />
|
||||
</linearGradient>
|
||||
<path
|
||||
className="st0"
|
||||
d="M64.6,237.9c2.4-2.4,5.7-3.8,9.2-3.8h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,237.9z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="SVGID_2_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="264.8291"
|
||||
y1="401.6014"
|
||||
x2="45.163"
|
||||
y2="-19.1475"
|
||||
gradientTransform="matrix(1 0 0 -1 0 314)"
|
||||
>
|
||||
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} />
|
||||
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} />
|
||||
</linearGradient>
|
||||
<path
|
||||
className="st1"
|
||||
d="M64.6,3.8C67.1,1.4,70.4,0,73.8,0h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,3.8z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="SVGID_3_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="312.5484"
|
||||
y1="376.688"
|
||||
x2="92.8822"
|
||||
y2="-44.061"
|
||||
gradientTransform="matrix(1 0 0 -1 0 314)"
|
||||
>
|
||||
<stop offset="0" style={{ stopColor: 'rgb(0, 255, 163)' }} />
|
||||
<stop offset="1" style={{ stopColor: 'rgb(220, 31, 255)' }} />
|
||||
</linearGradient>
|
||||
<path
|
||||
className="st2"
|
||||
d="M333.1,120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8,0-8.7,7-4.6,11.1l62.7,62.7c2.4,2.4,5.7,3.8,9.2,3.8h317.4 c5.8,0,8.7-7,4.6-11.1L333.1,120.1z"
|
||||
/>
|
||||
</svg>
|
||||
<Typography variant="h2" sx={{ color: 'text.secondary', ml: 1 }}>
|
||||
Solana
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
</style>
|
||||
<linearGradient
|
||||
id="SVGID_1_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="360.8791"
|
||||
y1="351.4553"
|
||||
x2="141.213"
|
||||
y2="-69.2936"
|
||||
gradientTransform="matrix(1 0 0 -1 0 314)"
|
||||
>
|
||||
<stop
|
||||
offset="0"
|
||||
style={{ stopColor: "rgb(0, 255, 163)" }}
|
||||
/>
|
||||
<stop
|
||||
offset="1"
|
||||
style={{ stopColor: "rgb(220, 31, 255)" }}
|
||||
/>
|
||||
</linearGradient>
|
||||
<path
|
||||
className="st0"
|
||||
d="M64.6,237.9c2.4-2.4,5.7-3.8,9.2-3.8h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,237.9z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="SVGID_2_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="264.8291"
|
||||
y1="401.6014"
|
||||
x2="45.163"
|
||||
y2="-19.1475"
|
||||
gradientTransform="matrix(1 0 0 -1 0 314)"
|
||||
>
|
||||
<stop
|
||||
offset="0"
|
||||
style={{ stopColor: "rgb(0, 255, 163)" }}
|
||||
/>
|
||||
<stop
|
||||
offset="1"
|
||||
style={{ stopColor: "rgb(220, 31, 255)" }}
|
||||
/>
|
||||
</linearGradient>
|
||||
<path
|
||||
className="st1"
|
||||
d="M64.6,3.8C67.1,1.4,70.4,0,73.8,0h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,3.8z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="SVGID_3_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="312.5484"
|
||||
y1="376.688"
|
||||
x2="92.8822"
|
||||
y2="-44.061"
|
||||
gradientTransform="matrix(1 0 0 -1 0 314)"
|
||||
>
|
||||
<stop
|
||||
offset="0"
|
||||
style={{ stopColor: "rgb(0, 255, 163)" }}
|
||||
/>
|
||||
<stop
|
||||
offset="1"
|
||||
style={{ stopColor: "rgb(220, 31, 255)" }}
|
||||
/>
|
||||
</linearGradient>
|
||||
<path
|
||||
className="st2"
|
||||
d="M333.1,120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8,0-8.7,7-4.6,11.1l62.7,62.7c2.4,2.4,5.7,3.8,9.2,3.8h317.4 c5.8,0,8.7-7,4.6-11.1L333.1,120.1z"
|
||||
/>
|
||||
</svg>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{ color: "text.secondary", ml: 1 }}
|
||||
>
|
||||
Solana
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
},
|
||||
"& h1, & h2": {
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default ConfigBox;
|
||||
@@ -7,7 +7,7 @@ import ConfigBox from "../ConfigBox";
|
||||
* ConfigRow is a styled container used to layout content in a row format with specific padding, border, and spacing.
|
||||
* It serves as the wrapper for ConfigBox, with the left section displaying the title and description,
|
||||
* and the right section displaying the children.
|
||||
*
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* return (
|
||||
@@ -19,31 +19,40 @@ import ConfigBox from "../ConfigBox";
|
||||
*/
|
||||
|
||||
const ConfigRow = ({ title, description, children }) => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2" variant="h2">
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography variant="body2" mt={theme.spacing(2)}>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)} mt={theme.spacing(4)}>
|
||||
{children}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
);
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
mt={theme.spacing(2)}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(15)}
|
||||
mt={theme.spacing(4)}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigRow.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ConfigRow;
|
||||
@@ -57,7 +57,7 @@ Dialog.propTypes = {
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
confirmationButtonLabel: PropTypes.string.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Dialog;
|
||||
@@ -1,19 +1,22 @@
|
||||
[class*="fallback__"] {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
[class*="fallback__"] h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[class*="fallback__"] button.MuiButtonBase-root,
|
||||
[class*="fallback__"] .check {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
[class*="fallback__"] button.MuiButtonBase-root {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
[class*="fallback__"] .check span.MuiTypography-root,
|
||||
[class*="fallback__"] button.MuiButtonBase-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
@@ -35,6 +38,7 @@
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.fallback__status > .MuiStack-root {
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { useSelector } from "react-redux";
|
||||
import Alert from "../Alert";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./index.css";
|
||||
import { useFetchSettings } from "../../Hooks/useFetchSettings";
|
||||
import { useFetchSettings } from "../../Hooks/settingsHooks";
|
||||
import { useState } from "react";
|
||||
/**
|
||||
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
|
||||
@@ -39,7 +39,11 @@ const Fallback = ({
|
||||
const { t } = useTranslation();
|
||||
const [settingsData, setSettingsData] = useState(undefined);
|
||||
|
||||
const [isLoading, error] = useFetchSettings({ setSettingsData });
|
||||
const [isLoading, error] = useFetchSettings({
|
||||
setSettingsData,
|
||||
setIsApiKeySet: () => {},
|
||||
setIsEmailPasswordSet: () => {},
|
||||
});
|
||||
// Custom warning message with clickable link
|
||||
const renderWarningMessage = () => {
|
||||
return (
|
||||
@@ -63,113 +67,138 @@ const Fallback = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
borderStyle: "dashed",
|
||||
minHeight: "calc(100vh - var(--env-var-spacing-2) * 2)",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
className={`fallback__${title?.trim().split(" ")[0]}`}
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
<Box
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderStyle: "dashed",
|
||||
height: {
|
||||
sm: "55vh",
|
||||
md: "50vh",
|
||||
lg: "70vh",
|
||||
xl: "58vh",
|
||||
},
|
||||
width: {
|
||||
sm: "90%",
|
||||
md: "70%",
|
||||
lg: "42%",
|
||||
},
|
||||
minHeight: {
|
||||
sm: theme.spacing(280),
|
||||
},
|
||||
padding: theme.spacing(10),
|
||||
}}
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Skeleton style={{ zIndex: 1 }} />
|
||||
) : (
|
||||
<SkeletonDark style={{ zIndex: 1 }} />
|
||||
)}
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
maxWidth={"300px"}
|
||||
zIndex={1}
|
||||
className={`fallback__${title?.trim().split(" ")[0]}`}
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
{mode === "light" ? (
|
||||
<Skeleton style={{ zIndex: 1 }} />
|
||||
) : (
|
||||
<SkeletonDark style={{ zIndex: 1 }} />
|
||||
)}
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{vowelStart ? "An" : "A"} {title} is used to:
|
||||
</Typography>
|
||||
{checks?.map((check, index) => (
|
||||
<Check
|
||||
text={check}
|
||||
key={`${title.trim().split(" ")[0]}-${index}`}
|
||||
outlined={true}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
{/* TODO - display a different fallback if user is not an admin*/}
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate(link)}
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
maxWidth={theme.spacing(180)}
|
||||
zIndex={1}
|
||||
>
|
||||
<Typography
|
||||
alignSelf="center"
|
||||
component="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
Let's create your first {title}
|
||||
</Button>
|
||||
{/* Bulk create of uptime monitors */}
|
||||
{title === "uptime monitor" && (
|
||||
{vowelStart ? "An" : "A"} {title} is used to:
|
||||
</Typography>
|
||||
{checks?.map((check, index) => (
|
||||
<Check
|
||||
text={check}
|
||||
key={`${title.trim().split(" ")[0]}-${index}`}
|
||||
outlined={true}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
{/* TODO - display a different fallback if user is not an admin*/}
|
||||
{isAdmin && (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate("/uptime/bulk-import")}
|
||||
sx={{ alignSelf: "center", mb: "2px" }}
|
||||
onClick={() => navigate(link)}
|
||||
>
|
||||
{t("bulkImport.fallbackPage")}
|
||||
Let's create your first {title}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Warning box for PageSpeed monitor */}
|
||||
{title === "pagespeed monitor" && showPageSpeedWarning && (
|
||||
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
"& .alert.row-stack": {
|
||||
backgroundColor: theme.palette.warningSecondary.main,
|
||||
borderColor: theme.palette.warningSecondary.lowContrast,
|
||||
"& .MuiTypography-root": {
|
||||
color: theme.palette.warningSecondary.contrastText,
|
||||
},
|
||||
"& .MuiBox-root > svg": {
|
||||
color: theme.palette.warningSecondary.contrastText,
|
||||
},
|
||||
},
|
||||
}}
|
||||
{/* Bulk create of uptime monitors */}
|
||||
{title === "uptime monitor" && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate("/uptime/bulk-import")}
|
||||
>
|
||||
{settingsData?.pagespeedKeySet === false && (
|
||||
<Alert
|
||||
variant="warning"
|
||||
hasIcon={true}
|
||||
body={renderWarningMessage()}
|
||||
/>
|
||||
)}
|
||||
{t("bulkImport.fallbackPage")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Warning box for PageSpeed monitor */}
|
||||
{title === "pagespeed monitor" && showPageSpeedWarning && (
|
||||
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
"& .alert.row-stack": {
|
||||
backgroundColor: theme.palette.warningSecondary.main,
|
||||
borderColor: theme.palette.warningSecondary.lowContrast,
|
||||
"& .MuiTypography-root": {
|
||||
color: theme.palette.warningSecondary.contrastText,
|
||||
},
|
||||
"& .MuiBox-root > svg": {
|
||||
color: theme.palette.warningSecondary.contrastText,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{settingsData?.pagespeedKeySet === false && (
|
||||
<Alert
|
||||
variant="warning"
|
||||
hasIcon={true}
|
||||
body={renderWarningMessage()}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,10 +42,14 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
|
||||
return header;
|
||||
}
|
||||
|
||||
return header + " | " + selected
|
||||
.map((value) => options.find((option) => option.value === value)?.label)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
return (
|
||||
header +
|
||||
" | " +
|
||||
selected
|
||||
.map((value) => options.find((option) => option.value === value)?.label)
|
||||
.filter(Boolean)
|
||||
.join(", ")
|
||||
);
|
||||
}}
|
||||
MenuProps={{
|
||||
anchorOrigin: {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const NetworkError = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
@@ -10,9 +13,9 @@ const NetworkError = () => {
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
Network error
|
||||
{t("common.toasts.networkError")}
|
||||
</Typography>
|
||||
<Typography>Please check your connection</Typography>
|
||||
<Typography>{t("common.toasts.checkConnection")}</Typography>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { logger } from "../../Utils/Logger";
|
||||
@@ -7,23 +7,35 @@ import { networkService } from "../../main";
|
||||
const withAdminCheck = (WrappedComponent) => {
|
||||
const WithAdminCheck = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
const [superAdminExists, setSuperAdminExists] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
networkService
|
||||
.doesSuperAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === true) {
|
||||
if (response?.data?.data === true) {
|
||||
navigate("/login");
|
||||
} else {
|
||||
setSuperAdminExists(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsChecking(false);
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
if (isChecking) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
isSuperAdmin={true}
|
||||
superAdminExists={superAdminExists}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import Dot from "../Dot";
|
||||
* @param {number} params.percentage - The percentage to display.
|
||||
* @returns {React.ElementType} Returns a div element with the host details.
|
||||
*/
|
||||
const Host = ({ url, title, percentageColor, percentage }) => {
|
||||
const Host = ({ url, title, percentageColor, percentage, showURL }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack>
|
||||
@@ -40,7 +40,7 @@ const Host = ({ url, title, percentageColor, percentage }) => {
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<span style={{ opacity: 0.6 }}>{url}</span>
|
||||
{showURL && <span style={{ opacity: 0.6 }}>{url}</span>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -50,6 +50,7 @@ Host.propTypes = {
|
||||
percentageColor: PropTypes.string,
|
||||
percentage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
showURL: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Host;
|
||||
@@ -0,0 +1,16 @@
|
||||
import i18n from "../../Utils/i18n";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
const I18nLoader = () => {
|
||||
const language = useSelector((state) => state.ui.language);
|
||||
|
||||
useEffect(() => {
|
||||
if (language && i18n.language !== language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default I18nLoader;
|
||||
@@ -105,7 +105,7 @@ const Checkbox = ({
|
||||
};
|
||||
|
||||
Checkbox.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
size: PropTypes.oneOf(["small", "medium", "large"]),
|
||||
|
||||
@@ -36,199 +36,198 @@ const ImageUpload = ({
|
||||
error,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [uploadComplete, setUploadComplete] = useState(false);
|
||||
const [completedFile, setCompletedFile] = useState(null);
|
||||
const [file, setFile] = useState(null);
|
||||
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
||||
const intervalRef = useRef(null);
|
||||
const [localError, setLocalError] = useState(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [uploadComplete, setUploadComplete] = useState(false);
|
||||
const [completedFile, setCompletedFile] = useState(null);
|
||||
const [file, setFile] = useState(null);
|
||||
const [progress, setProgress] = useState({ value: 0, isLoading: false });
|
||||
const intervalRef = useRef(null);
|
||||
const [localError, setLocalError] = useState(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const roundStyle = previewIsRound ? { borderRadius: "50%" } : {};
|
||||
|
||||
const handleImageChange = useCallback(
|
||||
(file) => {
|
||||
if (!file) return;
|
||||
(file) => {
|
||||
if (!file) return;
|
||||
|
||||
const isValidType = accept.some((type) =>
|
||||
file.type.includes(type)
|
||||
);
|
||||
const isValidSize = file.size <= maxSize;
|
||||
|
||||
if (!isValidType) {
|
||||
setLocalError(t('invalidFileFormat'));
|
||||
return;
|
||||
}
|
||||
if (!isValidSize) {
|
||||
setLocalError(t('invalidFileSize'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalError(null);
|
||||
|
||||
const previewFile = {
|
||||
src: URL.createObjectURL(file),
|
||||
name: file.name,
|
||||
file,
|
||||
};
|
||||
|
||||
setFile(previewFile);
|
||||
setProgress({ value: 0, isLoading: true });
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
const buffer = 12;
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
setUploadComplete(true);
|
||||
setCompletedFile(previewFile);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { value: prev.value + buffer, isLoading: true };
|
||||
});
|
||||
}, 120);
|
||||
},
|
||||
[maxSize, accept]
|
||||
);
|
||||
const isValidType = accept.some((type) => file.type.includes(type));
|
||||
const isValidSize = file.size <= maxSize;
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadComplete && completedFile) {
|
||||
onChange?.(completedFile);
|
||||
setUploadComplete(false);
|
||||
setCompletedFile(null);
|
||||
}
|
||||
}, [uploadComplete, completedFile, onChange]);
|
||||
if (!isValidType) {
|
||||
setLocalError(t("invalidFileFormat"));
|
||||
return;
|
||||
}
|
||||
if (!isValidSize) {
|
||||
setLocalError(t("invalidFileSize"));
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalError(null);
|
||||
|
||||
const previewFile = {
|
||||
src: URL.createObjectURL(file),
|
||||
name: file.name,
|
||||
file,
|
||||
};
|
||||
|
||||
setFile(previewFile);
|
||||
setProgress({ value: 0, isLoading: true });
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
const buffer = 12;
|
||||
if (prev.value + buffer >= 100) {
|
||||
clearInterval(intervalRef.current);
|
||||
setUploadComplete(true);
|
||||
setCompletedFile(previewFile);
|
||||
return { value: 100, isLoading: false };
|
||||
}
|
||||
return { value: prev.value + buffer, isLoading: true };
|
||||
});
|
||||
}, 120);
|
||||
},
|
||||
[maxSize, accept]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploadComplete && completedFile) {
|
||||
onChange?.(completedFile);
|
||||
setUploadComplete(false);
|
||||
setCompletedFile(null);
|
||||
}
|
||||
}, [uploadComplete, completedFile, onChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{src ? (
|
||||
<Stack direction="row" justifyContent="center">
|
||||
<Image
|
||||
alt="Uploaded preview"
|
||||
src={src}
|
||||
width="250px"
|
||||
height="250px"
|
||||
sx={{ ...roundStyle }}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
className="image-field-wrapper"
|
||||
mt={theme.spacing(8)}
|
||||
onDragEnter={() => setIsDragging(true)}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={() => setIsDragging(false)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: "fit-content",
|
||||
border: "dashed",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: isDragging
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.primary.lowContrast,
|
||||
backgroundColor: isDragging
|
||||
? "hsl(215, 87%, 51%, 0.05)"
|
||||
: "transparent",
|
||||
borderWidth: "2px",
|
||||
transition: "0.2s",
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="file"
|
||||
onChange={(e) => handleImageChange(e?.target?.files?.[0])}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiInputBase-input[type='file']": {
|
||||
opacity: 0,
|
||||
cursor: "pointer",
|
||||
maxWidth: "500px",
|
||||
minHeight: "175px",
|
||||
zIndex: 1,
|
||||
},
|
||||
"& fieldset": {
|
||||
padding: 0,
|
||||
border: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h2"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color="info"
|
||||
fontWeight={500}
|
||||
>
|
||||
{t('ClickUpload')}
|
||||
</Typography>{" "}
|
||||
or {t('DragandDrop')}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>({t('MaxSize')}: {Math.round(maxSize / 1024 / 1024)}MB)
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{(localError || progress.isLoading || progress.value !== 0) && (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
label={file?.name || "Upload failed"}
|
||||
size={file?.size}
|
||||
progress={progress.value}
|
||||
onClick={() => {
|
||||
clearInterval(intervalRef.current);
|
||||
setFile(null);
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setLocalError(null);
|
||||
onChange(undefined);
|
||||
}}
|
||||
error={localError || error}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{t('SupportedFormats')}: {accept.join(", ").toUpperCase()}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
<>
|
||||
{src ? (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Image
|
||||
alt="Uploaded preview"
|
||||
src={src}
|
||||
width="250px"
|
||||
height="250px"
|
||||
sx={{ ...roundStyle }}
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
className="image-field-wrapper"
|
||||
mt={theme.spacing(8)}
|
||||
onDragEnter={() => setIsDragging(true)}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={() => setIsDragging(false)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: "fit-content",
|
||||
border: "dashed",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: isDragging
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.primary.lowContrast,
|
||||
backgroundColor: isDragging ? "hsl(215, 87%, 51%, 0.05)" : "transparent",
|
||||
borderWidth: "2px",
|
||||
transition: "0.2s",
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
type="file"
|
||||
onChange={(e) => handleImageChange(e?.target?.files?.[0])}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiInputBase-input[type='file']": {
|
||||
opacity: 0,
|
||||
cursor: "pointer",
|
||||
maxWidth: "500px",
|
||||
minHeight: "175px",
|
||||
zIndex: 1,
|
||||
},
|
||||
"& fieldset": {
|
||||
padding: 0,
|
||||
border: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
gap="4px"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `solid ${theme.shape.borderThick}px ${theme.palette.primary.lowContrast}`,
|
||||
boxShadow: theme.shape.boxShadow,
|
||||
}}
|
||||
>
|
||||
<CloudUploadIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h2"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color="info"
|
||||
fontWeight={500}
|
||||
>
|
||||
{t("ClickUpload")}
|
||||
</Typography>{" "}
|
||||
or {t("DragandDrop")}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
({t("MaxSize")}: {Math.round(maxSize / 1024 / 1024)}MB)
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{(localError || progress.isLoading || progress.value !== 0) && (
|
||||
<ProgressUpload
|
||||
icon={<ImageIcon />}
|
||||
label={file?.name || "Upload failed"}
|
||||
size={file?.size}
|
||||
progress={progress.value}
|
||||
onClick={() => {
|
||||
clearInterval(intervalRef.current);
|
||||
setFile(null);
|
||||
setProgress({ value: 0, isLoading: false });
|
||||
setLocalError(null);
|
||||
onChange(undefined);
|
||||
}}
|
||||
error={localError || error}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{t("SupportedFormats")}: {accept.join(", ").toUpperCase()}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ImageUpload.propTypes = {
|
||||
@@ -240,4 +239,4 @@ ImageUpload.propTypes = {
|
||||
error: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
export default ImageUpload;
|
||||
@@ -16,26 +16,27 @@ import "./index.css";
|
||||
* size="small"
|
||||
* />
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.id - The id of the radio button.
|
||||
* @param {string} props.title - The title of the radio button.
|
||||
* @param {string} [props.desc] - The description of the radio button.
|
||||
* @param {string} [props.size="small"] - The size of the radio button.
|
||||
* @param {Object} props - The component
|
||||
* @param {string} id - The id of the radio button.
|
||||
* @param {string} title - The title of the radio button.
|
||||
* @param {string} [desc] - The description of the radio button.
|
||||
* @param {string} [size="small"] - The size of the radio button.
|
||||
* @returns {JSX.Element} - The rendered Radio component.
|
||||
*/
|
||||
|
||||
const Radio = (props) => {
|
||||
const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
className="custom-radio-button"
|
||||
checked={props.checked}
|
||||
value={props.value}
|
||||
name={name}
|
||||
checked={checked}
|
||||
value={value}
|
||||
control={
|
||||
<MUIRadio
|
||||
id={props.id}
|
||||
size={props.size}
|
||||
id={id}
|
||||
size={size}
|
||||
checkedIcon={<RadioChecked />}
|
||||
sx={{
|
||||
color: "transparent",
|
||||
@@ -49,16 +50,16 @@ const Radio = (props) => {
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onChange={props.onChange}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<>
|
||||
<Typography component="p">{props.title}</Typography>
|
||||
<Typography component="p">{title}</Typography>
|
||||
<Typography
|
||||
component="h6"
|
||||
mt={theme.spacing(1)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{props.desc}
|
||||
{desc}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
@@ -81,9 +82,14 @@ const Radio = (props) => {
|
||||
};
|
||||
|
||||
Radio.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
desc: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
checked: PropTypes.bool,
|
||||
value: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Radio;
|
||||
@@ -1,7 +1,17 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, ListItem, Autocomplete, TextField, Stack, Typography } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
ListItem,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Stack,
|
||||
Typography,
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Search component using Material UI's Autocomplete.
|
||||
@@ -60,24 +70,72 @@ const Search = ({
|
||||
onBlur,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [selectAll, setSelectAll] = React.useState(false);
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const enhancedOptions = React.useMemo(() => {
|
||||
return multiple && isAdorned
|
||||
? [
|
||||
{ [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
|
||||
...options,
|
||||
]
|
||||
: options;
|
||||
}, [multiple, isAdorned, options, filteredBy]);
|
||||
const isOptionSelected = (option) => {
|
||||
if (!multiple && !isAdorned) return false;
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => item._id === option._id);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const handleSelectAll = (isSelectAll) => {
|
||||
const newValue = isSelectAll ? [...options] : [];
|
||||
handleChange(newValue);
|
||||
setSelectAll(isSelectAll);
|
||||
};
|
||||
useEffect(() => {
|
||||
const allSelected =
|
||||
Array.isArray(value) && Array.isArray(options) && value.length === options.length;
|
||||
if (selectAll !== allSelected) setSelectAll(allSelected);
|
||||
}, [value, options]);
|
||||
return (
|
||||
<Autocomplete
|
||||
onBlur={onBlur}
|
||||
multiple={multiple}
|
||||
id={id}
|
||||
value={value}
|
||||
open={open}
|
||||
onOpen={() => setOpen(true)}
|
||||
onClose={(event, reason) => {
|
||||
if (reason === "blur" || reason === "escape") {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
inputValue={inputValue}
|
||||
onInputChange={(_, newValue) => {
|
||||
handleInputChange(newValue);
|
||||
}}
|
||||
onChange={(_, newValue) => {
|
||||
handleChange(newValue);
|
||||
if (multiple && isAdorned) {
|
||||
const hasSelectAllSelected =
|
||||
Array.isArray(newValue) && newValue.some((item) => item.isSelectAll);
|
||||
if (hasSelectAllSelected) {
|
||||
handleSelectAll(!selectAll);
|
||||
} else {
|
||||
handleChange(newValue);
|
||||
setSelectAll(Array.isArray(newValue) && newValue.length === options.length);
|
||||
}
|
||||
} else {
|
||||
handleChange(newValue);
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
freeSolo
|
||||
disabled={disabled}
|
||||
disableClearable
|
||||
options={options}
|
||||
options={enhancedOptions}
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
||||
renderInput={(params) => (
|
||||
@@ -102,27 +160,7 @@ const Search = ({
|
||||
...(endAdornment && { endAdornment: endAdornment }),
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
|
||||
{
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"& .MuiOutlinedInput-root": {
|
||||
paddingY: 0,
|
||||
},
|
||||
"& .MuiAutocomplete-tag": {
|
||||
// CAIO_REVIEW
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"& .MuiChip-deleteIcon": {
|
||||
color: theme.palette.primary.contrastText, // CAIO_REVIEW
|
||||
},
|
||||
}}
|
||||
sx={{}}
|
||||
/>
|
||||
{error && (
|
||||
<Typography
|
||||
@@ -140,6 +178,9 @@ const Search = ({
|
||||
</Stack>
|
||||
)}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
if (inputValue.trim() === "" && multiple && isAdorned) {
|
||||
return enhancedOptions;
|
||||
}
|
||||
const filtered = options.filter((option) =>
|
||||
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
@@ -155,6 +196,8 @@ const Search = ({
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
|
||||
const port = option["port"];
|
||||
const selected = isOptionSelected(option);
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
@@ -165,11 +208,33 @@ const Search = ({
|
||||
pointerEvents: "none",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
}
|
||||
: {}
|
||||
: option.isSelectAll
|
||||
? {
|
||||
fontWeight: "bold",
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{multiple && isAdorned && !option.noOptions && (
|
||||
<Checkbox
|
||||
checked={option.isSelectAll ? selectAll : selected}
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
"&.Mui-checked": {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{option[filteredBy] +
|
||||
(hasSecondaryLabel ? ` (${option[secondaryLabel]})` : "")}
|
||||
(hasSecondaryLabel
|
||||
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
|
||||
: "")}
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -12,9 +12,9 @@ import "./index.css";
|
||||
* @param {string} props.placeholder - The label of the select element.
|
||||
* @param {string} props.placeholder - The placeholder text when no option is selected.
|
||||
* @param {boolean} props.isHidden - Whether the placeholder should be hidden.
|
||||
* @param {string} props.value - The currently selected value.
|
||||
* @param {(string | number | boolean)} props.value - The currently selected value.
|
||||
* @param {object[]} props.items - The array of items to populate in the select dropdown.
|
||||
* @param {(string | number)} props.items._id - The unique identifier of each item.
|
||||
* @param {(string | number | boolean)} props.items._id - The unique identifier of each item.
|
||||
* @param {string} props.items.name - The display name of each item.
|
||||
* @param {function} props.onChange - The function to handle onChange event.
|
||||
* @param {object} props.sx - The custom styles object for MUI Select component.
|
||||
@@ -161,10 +161,13 @@ Select.propTypes = {
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
isHidden: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
|
||||
.isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
|
||||
.isRequired,
|
||||
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
|
||||
@@ -158,7 +158,15 @@ const StatusLabel = ({ status, text, customStyles }) => {
|
||||
};
|
||||
|
||||
StatusLabel.propTypes = {
|
||||
status: PropTypes.oneOf(["up", "down", "paused", "pending", "cannot resolve", "published", "unpublished"]),
|
||||
status: PropTypes.oneOf([
|
||||
"up",
|
||||
"down",
|
||||
"paused",
|
||||
"pending",
|
||||
"cannot resolve",
|
||||
"published",
|
||||
"unpublished",
|
||||
]),
|
||||
text: PropTypes.string,
|
||||
customStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -3,15 +3,17 @@ import { Box, MenuItem, Select, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setLanguage } from "../Features/UI/uiSlice";
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { language } = useSelector((state) => state.ui);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const handleChange = (event) => {
|
||||
const newLang = event.target.value;
|
||||
i18n.changeLanguage(newLang);
|
||||
dispatch(setLanguage(newLang));
|
||||
};
|
||||
|
||||
const languages = Object.keys(i18n.options.resources || {});
|
||||
@@ -21,107 +23,43 @@ const LanguageSelector = () => {
|
||||
value={language}
|
||||
onChange={handleChange}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 28,
|
||||
width: 64,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
fontSize: 10,
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: 16,
|
||||
height: 16,
|
||||
right: 4,
|
||||
top: "calc(50% - 8px)",
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
padding: "2px 20px 2px 8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontSize: 10,
|
||||
},
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
sx: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginTop: 1,
|
||||
width: 64,
|
||||
"& .MuiMenuItem-root": {
|
||||
padding: "2px 8px",
|
||||
minHeight: 28,
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
},
|
||||
}}
|
||||
sx={{ minWidth: 80 }}
|
||||
>
|
||||
{languages.map((lang) => {
|
||||
const flag = lang ? `fi fi-${lang}` : null;
|
||||
let parsedLang = lang === "en" ? "gb" : lang;
|
||||
|
||||
// Fix for Czech
|
||||
if (parsedLang === "cs") {
|
||||
parsedLang = "cz";
|
||||
}
|
||||
if (parsedLang.includes("-")) {
|
||||
parsedLang = parsedLang.split("-")[1].toLowerCase();
|
||||
}
|
||||
|
||||
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={lang}
|
||||
value={lang}
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastText,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
spacing={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
ml={0.5}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
width: 16,
|
||||
height: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"& img": {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: 0.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{flag && <span className={flag} />}
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ textTransform: "uppercase", fontSize: 10 }}
|
||||
sx={{ textTransform: "uppercase" }}
|
||||
>
|
||||
{lang}
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import BackgroundSVG from "../../../assets/Images/background.svg";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const AppLayout = ({ children }) => {
|
||||
const theme = useTheme();
|
||||
const ui = useSelector((state) => state.ui);
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
backgroundColor: theme.palette.primaryBackground.main,
|
||||
backgroundImage: ui?.mode === "dark" ? `url("${BackgroundSVG}")` : "none",
|
||||
backgroundSize: "100% 100%",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
color: theme.palette.primary.contrastText,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
AppLayout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -30,4 +30,4 @@
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Link as MuiLink, useTheme } from "@mui/material";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
@@ -10,7 +12,7 @@ import PropTypes from "prop-types";
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
const Link = ({ level, label, url }) => {
|
||||
const Link = ({ level, label, url, external = true }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const levelConfig = {
|
||||
@@ -49,11 +51,12 @@ const Link = ({ level, label, url }) => {
|
||||
const { sx, color } = levelConfig[level];
|
||||
return (
|
||||
<MuiLink
|
||||
href={url}
|
||||
component={external ? "a" : RouterLink}
|
||||
to={external ? undefined : url}
|
||||
href={external ? url : undefined}
|
||||
sx={{ width: "fit-content", ...sx }}
|
||||
color={color}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
{...(external && { target: "_blank", rel: "noreferrer" })}
|
||||
>
|
||||
{label}
|
||||
</MuiLink>
|
||||
@@ -64,6 +67,7 @@ Link.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
level: PropTypes.oneOf(["primary", "secondary", "tertiary", "error"]),
|
||||
label: PropTypes.string.isRequired,
|
||||
external: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Link;
|
||||
@@ -0,0 +1,116 @@
|
||||
import * as React from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import Grow from "@mui/material/Grow";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Popper from "@mui/material/Popper";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import MenuList from "@mui/material/MenuList";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { useExportMonitors } from "../../Hooks/monitorHooks";
|
||||
|
||||
const MonitorActions = ({ isLoading }) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const anchorRef = React.useRef(null);
|
||||
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [exportMonitors, isExporting] = useExportMonitors();
|
||||
|
||||
const options = [t("monitorActions.import"), t("monitorActions.export")];
|
||||
|
||||
const handleClick = async () => {
|
||||
if (selectedIndex === 0) {
|
||||
// Import
|
||||
navigate("/uptime/bulk-import");
|
||||
} else {
|
||||
// Export
|
||||
const [success, error] = await exportMonitors();
|
||||
if (!success) {
|
||||
createToast({ body: error || t("export.failed") });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuItemClick = (event, index) => {
|
||||
setSelectedIndex(index);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
color="accent"
|
||||
ref={anchorRef}
|
||||
aria-label="Monitor actions"
|
||||
disabled={isLoading || isExporting}
|
||||
>
|
||||
<Button onClick={handleClick}>{options[selectedIndex]}</Button>
|
||||
<Button
|
||||
size="small"
|
||||
aria-controls={open ? "split-button-menu" : undefined}
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
aria-label="select monitor action"
|
||||
aria-haspopup="menu"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper
|
||||
sx={{ zIndex: 1 }}
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === "bottom" ? "center top" : "center bottom",
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList
|
||||
id="split-button-menu"
|
||||
autoFocusItem
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
selected={index === selectedIndex}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorActions;
|
||||
@@ -4,14 +4,14 @@ import { useTheme } from "@emotion/react";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
|
||||
const MonitorCountHeader = ({
|
||||
shouldRender = true,
|
||||
isLoading = false,
|
||||
monitorCount,
|
||||
heading = "monitors",
|
||||
sx,
|
||||
children,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
if (!shouldRender) return <SkeletonLayout />;
|
||||
if (isLoading) return <SkeletonLayout />;
|
||||
|
||||
if (monitorCount === 1) {
|
||||
heading = "monitor";
|
||||
@@ -42,7 +42,7 @@ const MonitorCountHeader = ({
|
||||
};
|
||||
|
||||
MonitorCountHeader.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
monitorCount: PropTypes.number,
|
||||
heading: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
|
||||
@@ -3,18 +3,13 @@ import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import MonitorActions from "../MonitorActions";
|
||||
|
||||
const CreateMonitorHeader = ({
|
||||
isAdmin,
|
||||
label,
|
||||
isLoading = true,
|
||||
path,
|
||||
bulkPath,
|
||||
}) => {
|
||||
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
|
||||
// Use the provided label or fall back to the translated default
|
||||
|
||||
if (!isAdmin) return null;
|
||||
@@ -34,18 +29,7 @@ const CreateMonitorHeader = ({
|
||||
>
|
||||
{label || t("createNew")}
|
||||
</Button>
|
||||
{bulkPath && (
|
||||
<Button
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
navigate(`${bulkPath}`);
|
||||
}}
|
||||
>
|
||||
{t("bulkImport.title")}
|
||||
</Button>
|
||||
)}
|
||||
{/* {bulkPath && <MonitorActions isLoading={isLoading} />} */}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Status from "./status";
|
||||
import Skeleton from "./skeleton";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
|
||||
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { usePauseMonitor } from "../../Hooks/monitorHooks";
|
||||
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTestAllNotifications } from "../../Hooks/useNotifications";
|
||||
/**
|
||||
* MonitorDetailsControlHeader component displays the control header for monitor details.
|
||||
* It includes status display, pause/resume button, and a configure button for admins.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.path - The base path for navigation
|
||||
* @param {boolean} [props.isLoading=false] - Flag indicating if the data is loading
|
||||
* @param {boolean} [props.isAdmin=false] - Flag indicating if the user is an admin
|
||||
* @param {Object} props.monitor - The monitor object containing details
|
||||
* @param {Function} props.triggerUpdate - Function to trigger an update
|
||||
* @returns {JSX.Element} The rendered component
|
||||
*/
|
||||
const MonitorDetailsControlHeader = ({
|
||||
path,
|
||||
isLoading = false,
|
||||
isAdmin = false,
|
||||
monitor,
|
||||
triggerUpdate,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [pauseMonitor, isPausing, error] = usePauseMonitor();
|
||||
|
||||
const isTestNotificationsDisabled = monitor?.notifications?.length === 0;
|
||||
|
||||
const tooltipTitle = isTestNotificationsDisabled ? t("testNotificationsDisabled") : "";
|
||||
|
||||
// const [isSending, emailError, sendTestEmail] = useSendTestEmail();
|
||||
|
||||
const [testAllNotifications, isSending, errorAllNotifications] =
|
||||
useTestAllNotifications();
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Status monitor={monitor} />
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
key={monitor?._id}
|
||||
placement="bottom"
|
||||
title={tooltipTitle}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isSending}
|
||||
startIcon={<EmailIcon />}
|
||||
disabled={isTestNotificationsDisabled}
|
||||
onClick={() => {
|
||||
testAllNotifications({ monitorId: monitor?._id });
|
||||
}}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{t("sendTestNotifications")}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={(e) => {
|
||||
navigate(`/incidents/${monitor?._id}`);
|
||||
}}
|
||||
>
|
||||
{t("menu.incidents")}
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isPausing}
|
||||
startIcon={
|
||||
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||
}
|
||||
onClick={() => {
|
||||
pauseMonitor({
|
||||
monitorId: monitor?._id,
|
||||
triggerUpdate,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? "Pause" : "Resume"}
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<SettingsOutlinedIcon />}
|
||||
onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorDetailsControlHeader.propTypes = {
|
||||
path: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
triggerUpdate: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MonitorDetailsControlHeader;
|
||||
File renamed without changes.
@@ -0,0 +1,51 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import PulseDot from "../../Components/Animated/PulseDot";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Dot from "../../Components/Dot";
|
||||
// Utils
|
||||
import { formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
|
||||
import { formatMonitorUrl } from "../../Utils/utils";
|
||||
/**
|
||||
* Status component displays the status information of a monitor.
|
||||
* It includes the monitor's name, URL, and check interval.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component props
|
||||
* @param {Object} props.monitor - The monitor object containing details
|
||||
* @param {string} props.monitor.name - The name of the monitor
|
||||
* @param {string} props.monitor.url - The URL of the monitor
|
||||
* @param {number} props.monitor.interval - The interval at which the monitor checks
|
||||
* @returns {JSX.Element} The rendered component
|
||||
*/
|
||||
const Status = ({ monitor }) => {
|
||||
const theme = useTheme();
|
||||
const { statusColor, determineState } = useMonitorUtils();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Typography variant="monitorName">{monitor?.name}</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
<Typography variant="monitorUrl">{formatMonitorUrl(monitor?.url)}</Typography>
|
||||
<Dot />
|
||||
<Typography>
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Status.propTypes = {
|
||||
monitor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Status;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Button, Box } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
const ConfigButton = ({ shouldRender = true, monitorId, path }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
return (
|
||||
<Box alignSelf="flex-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/${path}/configure/${monitorId}`)}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg": {
|
||||
mr: theme.spacing(3),
|
||||
"& path": {
|
||||
/* Should always be contrastText for the button color */
|
||||
stroke: theme.palette.secondary.contrastText,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SettingsIcon /> Configure
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigButton.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
monitorId: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ConfigButton;
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import PulseDot from "../Animated/PulseDot";
|
||||
import Dot from "../Dot";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
|
||||
import { formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import ConfigButton from "./ConfigButton";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
|
||||
const theme = useTheme();
|
||||
const { statusColor, determineState } = useUtils();
|
||||
if (isLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack>
|
||||
<Typography variant="h1">{monitor?.name}</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
<Typography variant="h2"
|
||||
style={{fontFamily : "monospace" , fontWeight : 'bolder'}}
|
||||
>
|
||||
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Dot />
|
||||
<Typography>
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ConfigButton
|
||||
path={path}
|
||||
shouldRender={isAdmin}
|
||||
monitorId={monitor?._id}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorStatusHeader.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MonitorStatusHeader;
|
||||
@@ -0,0 +1,106 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
import Search from "../Inputs/Search";
|
||||
|
||||
// Utils
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => {
|
||||
// Local state
|
||||
const [notificationsSearch, setNotificationsSearch] = useState("");
|
||||
const [selectedNotifications, setSelectedNotifications] = useState([]);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSelectedNotifications(value);
|
||||
setMonitor((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
notifications: value.map((notification) => notification._id),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Handlers
|
||||
const handleDelete = (id) => {
|
||||
const updatedNotifications = selectedNotifications.filter(
|
||||
(notification) => notification._id !== id
|
||||
);
|
||||
|
||||
setSelectedNotifications(updatedNotifications);
|
||||
setMonitor((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
notifications: updatedNotifications.map((notification) => notification._id),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Setup
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (setNotifications) {
|
||||
const toSet = setNotifications.map((notification) => {
|
||||
return notifications.find((n) => n._id === notification);
|
||||
});
|
||||
setSelectedNotifications(toSet);
|
||||
}
|
||||
}, [setNotifications, notifications]);
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Search
|
||||
type="notifications"
|
||||
label="Notifications"
|
||||
options={notifications}
|
||||
filteredBy="notificationName"
|
||||
multiple={true}
|
||||
value={selectedNotifications}
|
||||
inputValue={notificationsSearch}
|
||||
handleInputChange={setNotificationsSearch}
|
||||
handleChange={(value) => {
|
||||
handleSearch(value);
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
flex={1}
|
||||
width="100%"
|
||||
>
|
||||
{selectedNotifications.map((notification, index) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
key={notification._id}
|
||||
width="100%"
|
||||
>
|
||||
<Typography
|
||||
flexGrow={1} // <-- This will take up all available horizontal space
|
||||
>
|
||||
{notification.notificationName}
|
||||
</Typography>
|
||||
<DeleteOutlineRoundedIcon
|
||||
onClick={() => {
|
||||
handleDelete(notification._id);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
{index < selectedNotifications.length - 1 && <Divider />}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationConfig.propTypes = {
|
||||
notifications: PropTypes.array,
|
||||
setMonitor: PropTypes.func,
|
||||
setNotifications: PropTypes.array,
|
||||
};
|
||||
|
||||
export default NotificationConfig;
|
||||
@@ -1,109 +1,115 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress
|
||||
} from "@mui/material";
|
||||
import { Typography, Box, Button, CircularProgress } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
|
||||
const TabComponent = ({
|
||||
type,
|
||||
integrations,
|
||||
handleIntegrationChange,
|
||||
handleInputChange,
|
||||
handleTestNotification,
|
||||
isLoading
|
||||
const TabComponent = ({
|
||||
type,
|
||||
integrations,
|
||||
handleIntegrationChange,
|
||||
handleInputChange,
|
||||
handleTestNotification,
|
||||
isLoading,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Helper to get the field state key (e.g., slackWebhook, telegramToken)
|
||||
const getFieldKey = (typeId, fieldId) => {
|
||||
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
|
||||
};
|
||||
|
||||
// Check if all fields have values to enable test button
|
||||
const areAllFieldsFilled = () => {
|
||||
return type.fields.every(field => {
|
||||
const fieldKey = getFieldKey(type.id, field.id);
|
||||
return integrations[fieldKey];
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="subtitle1" component="h4" sx={{
|
||||
fontWeight: 'bold',
|
||||
color: theme.palette.primary.contrastTextSecondary
|
||||
}}>
|
||||
{type.label}
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{
|
||||
mt: theme.spacing(0.5),
|
||||
mb: theme.spacing(1.5),
|
||||
color: theme.palette.primary.contrastTextTertiary
|
||||
}}>
|
||||
{type.description}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ pl: theme.spacing(1.5) }}>
|
||||
<Checkbox
|
||||
id={`enable-${type.id}`}
|
||||
label={t('notifications.enableNotifications', { platform: type.label })}
|
||||
isChecked={integrations[type.id]}
|
||||
onChange={(e) => handleIntegrationChange(type.id, e.target.checked)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{type.fields.map(field => {
|
||||
const fieldKey = getFieldKey(type.id, field.id);
|
||||
|
||||
return (
|
||||
<Box key={field.id} sx={{ mt: theme.spacing(1) }}>
|
||||
<Typography sx={{
|
||||
mb: theme.spacing(2),
|
||||
fontWeight: 'bold',
|
||||
color: theme.palette.primary.contrastTextSecondary
|
||||
}}>
|
||||
{field.label}
|
||||
</Typography>
|
||||
|
||||
<TextInput
|
||||
id={`${type.id}-${field.id}`}
|
||||
type={field.type}
|
||||
placeholder={field.placeholder}
|
||||
value={integrations[fieldKey]}
|
||||
onChange={(e) => handleInputChange(fieldKey, e.target.value)}
|
||||
disabled={!integrations[type.id] || isLoading}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<Box sx={{ mt: theme.spacing(1) }}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={() => handleTestNotification(type.id)}
|
||||
disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress
|
||||
size={theme.spacing(8)}
|
||||
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main}}
|
||||
/>
|
||||
) : null}
|
||||
{t('notifications.testNotification')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Helper to get the field state key (e.g., slackWebhook, telegramToken)
|
||||
const getFieldKey = (typeId, fieldId) => {
|
||||
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
|
||||
};
|
||||
|
||||
// Check if all fields have values to enable test button
|
||||
const areAllFieldsFilled = () => {
|
||||
return type.fields.every((field) => {
|
||||
const fieldKey = getFieldKey(type.id, field.id);
|
||||
return integrations[fieldKey];
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="h4"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
}}
|
||||
>
|
||||
{type.label}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
mt: theme.spacing(0.5),
|
||||
mb: theme.spacing(1.5),
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
}}
|
||||
>
|
||||
{type.description}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ pl: theme.spacing(1.5) }}>
|
||||
<Checkbox
|
||||
id={`enable-${type.id}`}
|
||||
label={t("notifications.enableNotifications", { platform: type.label })}
|
||||
isChecked={integrations[type.id]}
|
||||
onChange={(e) => handleIntegrationChange(type.id, e.target.checked)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{type.fields.map((field) => {
|
||||
const fieldKey = getFieldKey(type.id, field.id);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={field.id}
|
||||
sx={{ mt: theme.spacing(1) }}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
mb: theme.spacing(2),
|
||||
fontWeight: "bold",
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
}}
|
||||
>
|
||||
{field.label}
|
||||
</Typography>
|
||||
|
||||
<TextInput
|
||||
id={`${type.id}-${field.id}`}
|
||||
type={field.type}
|
||||
placeholder={field.placeholder}
|
||||
value={integrations[fieldKey]}
|
||||
onChange={(e) => handleInputChange(fieldKey, e.target.value)}
|
||||
disabled={!integrations[type.id] || isLoading}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<Box sx={{ mt: theme.spacing(1) }}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={() => handleTestNotification(type.id)}
|
||||
disabled={!integrations[type.id] || !areAllFieldsFilled() || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress
|
||||
size={theme.spacing(8)}
|
||||
sx={{ mr: theme.spacing(1), color: theme.palette.accent.main }}
|
||||
/>
|
||||
) : null}
|
||||
{t("notifications.testNotification")}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabComponent;
|
||||
export default TabComponent;
|
||||
@@ -5,7 +5,7 @@ import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* TabPanel component that displays content for the selected tab.
|
||||
*
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - The component props.
|
||||
* @param {React.ReactNode} props.children - The content to be displayed when this tab panel is selected.
|
||||
@@ -15,32 +15,27 @@ import { useTheme } from "@emotion/react";
|
||||
* @returns {React.ReactElement|null} The rendered tab panel or null if not selected.
|
||||
*/
|
||||
function TabPanel({ children, value, index, ...other }) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`notification-tabpanel-${index}`}
|
||||
aria-labelledby={`notification-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ pt: theme.spacing(3) }}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`notification-tabpanel-${index}`}
|
||||
aria-labelledby={`notification-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box sx={{ pt: theme.spacing(3) }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: PropTypes.node,
|
||||
|
||||
children: PropTypes.node,
|
||||
|
||||
index: PropTypes.number.isRequired,
|
||||
|
||||
value: PropTypes.number.isRequired
|
||||
index: PropTypes.number.isRequired,
|
||||
|
||||
value: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default TabPanel;
|
||||
export default TabPanel;
|
||||
@@ -1,128 +1,129 @@
|
||||
import { useState } from 'react';
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { networkService } from '../../../Utils/NetworkService';
|
||||
import { createToast } from '../../../Utils/toastUtils';
|
||||
import { networkService } from "../../../Utils/NetworkService";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
|
||||
// Define constants for notification types to avoid magic values
|
||||
const NOTIFICATION_TYPES = {
|
||||
SLACK: 'slack',
|
||||
DISCORD: 'discord',
|
||||
TELEGRAM: 'telegram',
|
||||
WEBHOOK: 'webhook'
|
||||
SLACK: "slack",
|
||||
DISCORD: "discord",
|
||||
TELEGRAM: "telegram",
|
||||
WEBHOOK: "webhook",
|
||||
};
|
||||
|
||||
// Define constants for field IDs
|
||||
const FIELD_IDS = {
|
||||
WEBHOOK: 'webhook',
|
||||
TOKEN: 'token',
|
||||
CHAT_ID: 'chatId',
|
||||
URL: 'url'
|
||||
WEBHOOK: "webhook",
|
||||
TOKEN: "token",
|
||||
CHAT_ID: "chatId",
|
||||
URL: "url",
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for notification-related operations
|
||||
*/
|
||||
const useNotifications = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* Send a test notification
|
||||
* @param {string} type - The notification type (slack, discord, telegram, webhook)
|
||||
* @param {object} config - Configuration object with necessary params
|
||||
*/
|
||||
const sendTestNotification = async (type, config) => {
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
// Validation based on notification type
|
||||
let payload = { platform: type };
|
||||
let isValid = true;
|
||||
let errorMessage = '';
|
||||
|
||||
switch(type) {
|
||||
case NOTIFICATION_TYPES.SLACK:
|
||||
payload.webhookUrl = config.webhook;
|
||||
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
|
||||
isValid = false;
|
||||
errorMessage = t('notifications.slack.webhookRequired');
|
||||
}
|
||||
break;
|
||||
|
||||
case NOTIFICATION_TYPES.DISCORD:
|
||||
payload.webhookUrl = config.webhook;
|
||||
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
|
||||
isValid = false;
|
||||
errorMessage = t('notifications.discord.webhookRequired');
|
||||
}
|
||||
break;
|
||||
|
||||
case NOTIFICATION_TYPES.TELEGRAM:
|
||||
payload.botToken = config.token;
|
||||
payload.chatId = config.chatId;
|
||||
if (typeof payload.botToken === 'undefined' || payload.botToken === '' ||
|
||||
typeof payload.chatId === 'undefined' || payload.chatId === '') {
|
||||
isValid = false;
|
||||
errorMessage = t('notifications.telegram.fieldsRequired');
|
||||
}
|
||||
break;
|
||||
|
||||
case NOTIFICATION_TYPES.WEBHOOK:
|
||||
payload.webhookUrl = config.url;
|
||||
payload.platform = NOTIFICATION_TYPES.SLACK;
|
||||
if (typeof payload.webhookUrl === 'undefined' || payload.webhookUrl === '') {
|
||||
isValid = false;
|
||||
errorMessage = t('notifications.webhook.urlRequired');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
isValid = false;
|
||||
errorMessage = t('notifications.unsupportedType');
|
||||
}
|
||||
/**
|
||||
* Send a test notification
|
||||
* @param {string} type - The notification type (slack, discord, telegram, webhook)
|
||||
* @param {object} config - Configuration object with necessary params
|
||||
*/
|
||||
const sendTestNotification = async (type, config) => {
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
// If validation fails, show error and return
|
||||
if (isValid === false) {
|
||||
createToast({
|
||||
body: errorMessage,
|
||||
variant: "error"
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
// Validation based on notification type
|
||||
let payload = { platform: type };
|
||||
let isValid = true;
|
||||
let errorMessage = "";
|
||||
|
||||
try {
|
||||
const response = await networkService.testNotification({
|
||||
platform: type,
|
||||
payload: payload
|
||||
});
|
||||
|
||||
if (response.data.success === true) {
|
||||
createToast({
|
||||
body: t('notifications.testSuccess'),
|
||||
variant: "info"
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.data.msg || t('notifications.testFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error.response?.data?.msg || error.message || t('notifications.networkError');
|
||||
createToast({
|
||||
body: `${t('notifications.testFailed')}: ${errorMsg}`,
|
||||
variant: "error"
|
||||
});
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
switch (type) {
|
||||
case NOTIFICATION_TYPES.SLACK:
|
||||
payload.webhookUrl = config.webhook;
|
||||
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
|
||||
isValid = false;
|
||||
errorMessage = t("notifications.slack.webhookRequired");
|
||||
}
|
||||
break;
|
||||
|
||||
return [
|
||||
loading,
|
||||
error,
|
||||
sendTestNotification
|
||||
];
|
||||
case NOTIFICATION_TYPES.DISCORD:
|
||||
payload.webhookUrl = config.webhook;
|
||||
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
|
||||
isValid = false;
|
||||
errorMessage = t("notifications.discord.webhookRequired");
|
||||
}
|
||||
break;
|
||||
|
||||
case NOTIFICATION_TYPES.TELEGRAM:
|
||||
payload.botToken = config.token;
|
||||
payload.chatId = config.chatId;
|
||||
if (
|
||||
typeof payload.botToken === "undefined" ||
|
||||
payload.botToken === "" ||
|
||||
typeof payload.chatId === "undefined" ||
|
||||
payload.chatId === ""
|
||||
) {
|
||||
isValid = false;
|
||||
errorMessage = t("notifications.telegram.fieldsRequired");
|
||||
}
|
||||
break;
|
||||
|
||||
case NOTIFICATION_TYPES.WEBHOOK:
|
||||
payload.webhookUrl = config.url;
|
||||
payload.platform = NOTIFICATION_TYPES.SLACK;
|
||||
if (typeof payload.webhookUrl === "undefined" || payload.webhookUrl === "") {
|
||||
isValid = false;
|
||||
errorMessage = t("notifications.webhook.urlRequired");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
isValid = false;
|
||||
errorMessage = t("notifications.unsupportedType");
|
||||
}
|
||||
|
||||
// If validation fails, show error and return
|
||||
if (isValid === false) {
|
||||
createToast({
|
||||
body: errorMessage,
|
||||
variant: "error",
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await networkService.testNotification({
|
||||
platform: type,
|
||||
payload: payload,
|
||||
});
|
||||
|
||||
if (response.data.success === true) {
|
||||
createToast({
|
||||
body: t("notifications.testSuccess"),
|
||||
variant: "info",
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.data.msg || t("notifications.testFailed"));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg =
|
||||
error.response?.data?.msg || error.message || t("notifications.networkError");
|
||||
createToast({
|
||||
body: `${t("notifications.testFailed")}: ${errorMsg}`,
|
||||
variant: "error",
|
||||
});
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [loading, error, sendTestNotification];
|
||||
};
|
||||
|
||||
export default useNotifications;
|
||||
export default useNotifications;
|
||||
@@ -93,7 +93,7 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
|
||||
<Typography
|
||||
component="h2"
|
||||
mb={theme.spacing(1.5)}
|
||||
sx={{ wordBreak: 'break-all' }}
|
||||
sx={{ wordBreak: "break-all" }}
|
||||
>
|
||||
{error ? error : label}
|
||||
</Typography>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* @param {Object} props - The props passed to the ProtectedDistributedUptimeRoute component.
|
||||
* @param {React.ReactNode} props.children - The children to render if the user is authenticated.
|
||||
* @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
|
||||
*/
|
||||
|
||||
const ProtectedDistributedUptimeRoute = ({ children }) => {
|
||||
const distributedUptimeEnabled = useSelector(
|
||||
(state) => state.ui.distributedUptimeEnabled
|
||||
);
|
||||
|
||||
return distributedUptimeEnabled === true ? (
|
||||
children
|
||||
) : (
|
||||
<Navigate
|
||||
to="/uptime"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProtectedDistributedUptimeRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default ProtectedDistributedUptimeRoute;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ProtectedRoute is a wrapper component that ensures only authenticated users
|
||||
* can access the wrapped content. It checks authentication status (e.g., from Redux or Context).
|
||||
* If the user is authenticated, it renders the children; otherwise, it redirects to the login page.
|
||||
*
|
||||
* @param {Object} props - The props passed to the ProtectedRoute component.
|
||||
* @param {React.ReactNode} props.children - The children to render if the user is authenticated.
|
||||
* @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
|
||||
*/
|
||||
|
||||
const RoleProtectedRoute = ({ roles, children }) => {
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const userRoles = authState?.user?.role || [];
|
||||
const canAccess = userRoles.some((role) => roles.includes(role));
|
||||
|
||||
return canAccess ? (
|
||||
children
|
||||
) : (
|
||||
<Navigate
|
||||
to="/uptime"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
RoleProtectedRoute.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
roles: PropTypes.array,
|
||||
};
|
||||
|
||||
export default RoleProtectedRoute;
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListSubheader,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Stack,
|
||||
@@ -23,7 +22,6 @@ import UserSvg from "../../assets/icons/user.svg?react";
|
||||
import TeamSvg from "../../assets/icons/user-two.svg?react";
|
||||
import LogoutSvg from "../../assets/icons/logout.svg?react";
|
||||
import Support from "../../assets/icons/support.svg?react";
|
||||
import Account from "../../assets/icons/user-edit.svg?react";
|
||||
import Maintenance from "../../assets/icons/maintenance.svg?react";
|
||||
import Monitors from "../../assets/icons/monitors.svg?react";
|
||||
import Incidents from "../../assets/icons/incidents.svg?react";
|
||||
@@ -37,10 +35,11 @@ import ArrowLeft from "../../assets/icons/left-arrow.svg?react";
|
||||
import DotsVertical from "../../assets/icons/dots-vertical.svg?react";
|
||||
import ChangeLog from "../../assets/icons/changeLog.svg?react";
|
||||
import Docs from "../../assets/icons/docs.svg?react";
|
||||
import Folder from "../../assets/icons/folder.svg?react";
|
||||
import StatusPages from "../../assets/icons/status-pages.svg?react";
|
||||
import Discussions from "../../assets/icons/discussions.svg?react";
|
||||
import DistributedUptimeIcon from "../../assets/icons/distributed-uptime.svg?react";
|
||||
import Notifications from "../../assets/icons/notifications.svg?react";
|
||||
import Logs from "../../assets/icons/logs.svg?react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
// Utils
|
||||
@@ -50,22 +49,25 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { toggleSidebar } from "../../Features/UI/uiSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { TurnedIn } from "@mui/icons-material";
|
||||
import { rules } from "eslint-plugin-react-refresh";
|
||||
|
||||
const getMenu = (t) => [
|
||||
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
|
||||
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
|
||||
|
||||
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
|
||||
{
|
||||
name: t("menu.distributedUptime"),
|
||||
path: "distributed-uptime",
|
||||
icon: <DistributedUptimeIcon />,
|
||||
name: t("menu.notifications"),
|
||||
path: "notifications",
|
||||
icon: <Notifications />,
|
||||
},
|
||||
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
|
||||
|
||||
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
|
||||
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
|
||||
// { name: t("menu.integrations"), path: "integrations", icon: <Integrations /> },
|
||||
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
|
||||
|
||||
{
|
||||
name: t("menu.settings"),
|
||||
icon: <Settings />,
|
||||
@@ -95,14 +97,13 @@ const URL_MAP = {
|
||||
support: "https://discord.com/invite/NAb6H3UTjK",
|
||||
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
|
||||
docs: "https://bluewavelabs.gitbook.io/checkmate",
|
||||
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
|
||||
changelog: "https://github.com/bluewave-labs/checkmate/releases",
|
||||
};
|
||||
|
||||
const PATH_MAP = {
|
||||
monitors: "Dashboard",
|
||||
pagespeed: "Dashboard",
|
||||
infrastructure: "Dashboard",
|
||||
["distributed-uptime"]: "Dashboard",
|
||||
account: "Account",
|
||||
settings: "Settings",
|
||||
};
|
||||
@@ -121,8 +122,7 @@ function Sidebar() {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
|
||||
const menu = getMenu(t);
|
||||
|
||||
const otherMenuItems = getOtherMenuItems(t);
|
||||
const accountMenuItems = getAccountMenuItems(t);
|
||||
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
|
||||
@@ -130,12 +130,17 @@ function Sidebar() {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [popup, setPopup] = useState();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const distributedUptimeEnabled = useSelector(
|
||||
(state) => state.ui.distributedUptimeEnabled
|
||||
);
|
||||
|
||||
const sidebarRef = useRef(null);
|
||||
const [sidebarReady, setSidebarReady] = useState(false);
|
||||
const TRANSITION_DURATION = 200;
|
||||
let menu = getMenu(t);
|
||||
menu = menu.filter((item) => {
|
||||
if (item.path === "logs") {
|
||||
return user.role?.includes("admin") || user.role?.includes("superadmin");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!collapsed) {
|
||||
@@ -147,7 +152,7 @@ function Sidebar() {
|
||||
} else {
|
||||
setSidebarReady(false);
|
||||
}
|
||||
}, [collapsed]);
|
||||
}, [collapsed]);
|
||||
|
||||
const renderAccountMenuItems = () => {
|
||||
let filteredAccountMenuItems = [...accountMenuItems];
|
||||
@@ -170,8 +175,8 @@ function Sidebar() {
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
onClick={() => {
|
||||
closePopup()
|
||||
navigate(item.path)
|
||||
closePopup();
|
||||
navigate(item.path);
|
||||
}}
|
||||
sx={{
|
||||
gap: theme.spacing(2),
|
||||
@@ -200,7 +205,6 @@ function Sidebar() {
|
||||
const logout = async () => {
|
||||
// Clear auth state
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
@@ -231,10 +235,9 @@ function Sidebar() {
|
||||
*/
|
||||
sx={{
|
||||
position: "relative",
|
||||
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: 0,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
"& :is(p, span, .MuiListSubheader-root)": {
|
||||
/*
|
||||
Text color for unselected menu items and menu headings
|
||||
@@ -346,7 +349,7 @@ function Sidebar() {
|
||||
mt={theme.spacing(2)}
|
||||
sx={{ opacity: 0.8, fontWeight: 500 }}
|
||||
>
|
||||
Checkmate
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -375,12 +378,6 @@ function Sidebar() {
|
||||
}}
|
||||
>
|
||||
{menu.map((item) => {
|
||||
if (
|
||||
item.path === "distributed-uptime" &&
|
||||
distributedUptimeEnabled === false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return item.path ? (
|
||||
/* If item has a path */
|
||||
<Tooltip
|
||||
@@ -732,11 +729,15 @@ function Sidebar() {
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography sx={{ textTransform: "capitalize" }}>
|
||||
{authState.user?.role?.includes("superadmin") ? t("roles.superAdmin") :
|
||||
authState.user?.role?.includes("admin") ? t("roles.admin") :
|
||||
authState.user?.role?.includes("user") ? t("roles.teamMember") :
|
||||
authState.user?.role?.includes("demo") ? t("roles.demoUser") :
|
||||
authState.user?.role}
|
||||
{authState.user?.role?.includes("superadmin")
|
||||
? t("roles.superAdmin")
|
||||
: authState.user?.role?.includes("admin")
|
||||
? t("roles.admin")
|
||||
: authState.user?.role?.includes("user")
|
||||
? t("roles.teamMember")
|
||||
: authState.user?.role?.includes("demo")
|
||||
? t("roles.demoUser")
|
||||
: authState.user?.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
|
||||
@@ -1,101 +1,107 @@
|
||||
import React from 'react';
|
||||
import { Typography, IconButton, Stack, Box } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { setStarPromptOpen } from '../../Features/UI/uiSlice';
|
||||
import React from "react";
|
||||
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { setStarPromptOpen } from "../../Features/UI/uiSlice";
|
||||
|
||||
const StarPrompt = ({
|
||||
repoUrl = 'https://github.com/bluewave-labs/checkmate'
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isOpen = useSelector((state) => state.ui?.starPromptOpen ?? true);
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setStarPromptOpen(false));
|
||||
};
|
||||
const handleClose = () => {
|
||||
dispatch(setStarPromptOpen(false));
|
||||
};
|
||||
|
||||
const handleStarClick = () => {
|
||||
window.open(repoUrl, '_blank');
|
||||
};
|
||||
const handleStarClick = () => {
|
||||
window.open(repoUrl, "_blank");
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
sx={{
|
||||
width: '100%',
|
||||
padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
|
||||
borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderRadius: 0,
|
||||
gap: theme.spacing(1.5),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" width="100%" pl={theme.spacing(4)}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color: mode === 'dark' ? theme.palette.primary.contrastText : theme.palette.text.primary,
|
||||
mt: theme.spacing(3)
|
||||
}}
|
||||
>
|
||||
{t('starPromptTitle')}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
size="small"
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
padding: 0,
|
||||
marginTop: theme.spacing(-5),
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
opacity: 0.8
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: '1.25rem' }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
sx={{
|
||||
width: "100%",
|
||||
padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
|
||||
borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderRadius: 0,
|
||||
gap: theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
pl={theme.spacing(4)}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
color:
|
||||
mode === "dark"
|
||||
? theme.palette.primary.contrastText
|
||||
: theme.palette.text.primary,
|
||||
mt: theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
{t("starPromptTitle")}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
size="small"
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
padding: 0,
|
||||
marginTop: theme.spacing(-5),
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent",
|
||||
opacity: 0.8,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: "1.25rem" }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: '0.938rem',
|
||||
lineHeight: 1.5,
|
||||
mb: 1,
|
||||
px: theme.spacing(4)
|
||||
}}
|
||||
>
|
||||
{t('starPromptDescription')}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: "0.938rem",
|
||||
lineHeight: 1.5,
|
||||
mb: 1,
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{t("starPromptDescription")}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
component="img"
|
||||
src={`https://img.shields.io/github/stars/bluewave-labs/checkmate?label=checkmate&style=social${mode === 'dark' ? '&color=white' : ''}`}
|
||||
alt="GitHub stars"
|
||||
onClick={handleStarClick}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
transform: 'scale(0.65)',
|
||||
transformOrigin: 'left center',
|
||||
'&:hover': {
|
||||
opacity: 0.8
|
||||
},
|
||||
pl: theme.spacing(4),
|
||||
filter: mode === 'dark' ? 'invert(1)' : 'none'
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
<Box
|
||||
component="img"
|
||||
src={`https://img.shields.io/github/stars/bluewave-labs/checkmate?label=checkmate&style=social${mode === "dark" ? "&color=white" : ""}`}
|
||||
alt="GitHub stars"
|
||||
onClick={handleStarClick}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
transform: "scale(0.65)",
|
||||
transformOrigin: "left center",
|
||||
"&:hover": {
|
||||
opacity: 0.8,
|
||||
},
|
||||
pl: theme.spacing(4),
|
||||
filter: mode === "dark" ? "invert(1)" : "none",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default StarPrompt;
|
||||
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
|
||||
import Image from "../Image";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
|
||||
import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
|
||||
|
||||
/**
|
||||
* StatBox Component
|
||||
@@ -41,7 +41,7 @@ const StatBox = ({
|
||||
sx,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { statusToTheme } = useUtils();
|
||||
const { statusToTheme } = useMonitorUtils();
|
||||
const themeColor = statusToTheme[status];
|
||||
|
||||
const statusBoxStyles = gradient
|
||||
@@ -80,7 +80,7 @@ const StatBox = ({
|
||||
};
|
||||
|
||||
const responsiveWidths = {
|
||||
default: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
||||
default: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
|
||||
md: `calc(50% - (1 * ${theme.spacing(8)} / 2))`,
|
||||
};
|
||||
|
||||
@@ -136,8 +136,8 @@ const StatBox = ({
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
heading: PropTypes.string.isRequired,
|
||||
subHeading: PropTypes.node.isRequired,
|
||||
heading: PropTypes.string,
|
||||
subHeading: PropTypes.node,
|
||||
gradient: PropTypes.bool,
|
||||
status: PropTypes.string,
|
||||
sx: PropTypes.object,
|
||||
|
||||
@@ -4,7 +4,7 @@ import SkeletonLayout from "./skeleton";
|
||||
// Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => {
|
||||
const StatusBoxes = ({ shouldRender = true, flexWrap = "nowrap", children }) => {
|
||||
const theme = useTheme();
|
||||
if (!shouldRender) {
|
||||
return (
|
||||
|
||||
@@ -20,7 +20,11 @@ const CustomTabList = ({ value, onChange, children, ...props }) => {
|
||||
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
|
||||
}}
|
||||
>
|
||||
<TabList value={value} onChange={onChange} {...props}>
|
||||
<TabList
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import Alert from "../../Alert";
|
||||
import { update } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@@ -61,7 +61,7 @@ const PasswordPanel = () => {
|
||||
[name]: true,
|
||||
};
|
||||
|
||||
const validation = credentials.validate(
|
||||
const validation = newOrChangedCredentials.validate(
|
||||
{ ...updatedData },
|
||||
{ abortEarly: false, context: { password: updatedData.newPassword } }
|
||||
);
|
||||
@@ -79,7 +79,7 @@ const PasswordPanel = () => {
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { error } = credentials.validate(localData, {
|
||||
const { error } = newOrChangedCredentials.validate(localData, {
|
||||
abortEarly: false,
|
||||
context: { password: localData.newPassword },
|
||||
});
|
||||
@@ -94,7 +94,10 @@ const PasswordPanel = () => {
|
||||
const action = await dispatch(update({ localData }));
|
||||
if (action.payload.success) {
|
||||
createToast({
|
||||
body: t("passwordPanel.passwordChangedSuccess", "Your password was changed successfully."),
|
||||
body: t(
|
||||
"passwordPanel.passwordChangedSuccess",
|
||||
"Your password was changed successfully."
|
||||
),
|
||||
});
|
||||
setLocalData({
|
||||
password: "",
|
||||
@@ -104,7 +107,10 @@ const PasswordPanel = () => {
|
||||
} else {
|
||||
// TODO: Check for other errors?
|
||||
createToast({
|
||||
body: t("passwordPanel.passwordInputIncorrect", "Your password input was incorrect."),
|
||||
body: t(
|
||||
"passwordPanel.passwordInputIncorrect",
|
||||
"Your password input was incorrect."
|
||||
),
|
||||
});
|
||||
setErrors({ password: "*" + action.payload.msg + "." });
|
||||
}
|
||||
@@ -153,7 +159,10 @@ const PasswordPanel = () => {
|
||||
<TextInput
|
||||
type="password"
|
||||
id="edit-current-password"
|
||||
placeholder={t("passwordPanel.enterCurrentPassword", "Enter your current password")}
|
||||
placeholder={t(
|
||||
"passwordPanel.enterCurrentPassword",
|
||||
"Enter your current password"
|
||||
)}
|
||||
autoComplete="current-password"
|
||||
value={localData.password}
|
||||
onChange={handleChange}
|
||||
@@ -205,7 +214,7 @@ const PasswordPanel = () => {
|
||||
<TextInput
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
placeholder={t("confirmPassword", "Confirm password")}
|
||||
placeholder={t("confirmPassword")}
|
||||
autoComplete="new-password"
|
||||
value={localData.confirm}
|
||||
onChange={handleChange}
|
||||
@@ -219,7 +228,10 @@ const PasswordPanel = () => {
|
||||
<Box sx={{ maxWidth: "70ch" }}>
|
||||
<Alert
|
||||
variant="warning"
|
||||
body={t("passwordPanel.passwordRequirements", "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.")}
|
||||
body={t(
|
||||
"passwordPanel.passwordRequirements",
|
||||
"New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character."
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -5,10 +5,9 @@ import { Box, Button, Divider, Stack, Typography } from "@mui/material";
|
||||
import Avatar from "../../Avatar";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import ImageUpload from "../../Inputs/ImageUpload";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
@@ -58,7 +57,7 @@ const ProfilePanel = () => {
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
validateField({ [name]: value }, credentials, name);
|
||||
validateField({ [name]: value }, newOrChangedCredentials, name);
|
||||
};
|
||||
|
||||
// Validates input against provided schema and updates error state
|
||||
@@ -85,13 +84,13 @@ const ProfilePanel = () => {
|
||||
// Resets picture-related states and clears interval
|
||||
const removePicture = () => {
|
||||
errors["picture"] && clearError("picture");
|
||||
setFile(undefined);
|
||||
setFile(undefined);
|
||||
setLocalData((prev) => ({
|
||||
...prev,
|
||||
file: undefined,
|
||||
deleteProfileImage: true,
|
||||
deleteProfileImage: true,
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
// Opens the picture update modal
|
||||
const openPictureModal = () => {
|
||||
@@ -102,9 +101,9 @@ const ProfilePanel = () => {
|
||||
// Closes the picture update modal and resets related states
|
||||
const closePictureModal = () => {
|
||||
if (errors["picture"]) clearError("picture");
|
||||
setFile(undefined);
|
||||
setIsOpen("");
|
||||
};
|
||||
setFile(undefined);
|
||||
setIsOpen("");
|
||||
};
|
||||
|
||||
// Updates profile image displayed on UI
|
||||
const handleUpdatePicture = () => {
|
||||
@@ -116,24 +115,23 @@ const ProfilePanel = () => {
|
||||
setIsOpen("");
|
||||
if (errors["unchanged"]) clearError("unchanged");
|
||||
};
|
||||
|
||||
|
||||
// Handles form submission to update user profile
|
||||
const handleSaveProfile = async (event) => {
|
||||
event.preventDefault();
|
||||
const nameChanged =
|
||||
localData.firstName !== user.firstName ||
|
||||
localData.lastName !== user.lastName;
|
||||
localData.firstName !== user.firstName || localData.lastName !== user.lastName;
|
||||
|
||||
const avatarChanged =
|
||||
localData.deleteProfileImage === true ||
|
||||
(localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`);
|
||||
localData.deleteProfileImage === true ||
|
||||
(localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`);
|
||||
|
||||
if (!nameChanged && !avatarChanged) {
|
||||
createToast({
|
||||
body: "Unable to update profile — no changes detected.",
|
||||
});
|
||||
setErrors({ unchanged: "unable to update profile" });
|
||||
return;
|
||||
createToast({
|
||||
body: "Unable to update profile — no changes detected.",
|
||||
});
|
||||
setErrors({ unchanged: "unable to update profile" });
|
||||
return;
|
||||
}
|
||||
|
||||
const action = await dispatch(update({ localData }));
|
||||
@@ -163,7 +161,6 @@ const ProfilePanel = () => {
|
||||
const action = await dispatch(deleteUser());
|
||||
if (action.payload.success) {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
} else {
|
||||
if (action.payload) {
|
||||
// dispatch errors
|
||||
@@ -205,7 +202,7 @@ const ProfilePanel = () => {
|
||||
>
|
||||
{/* This 0.9 is a bit magic numbering, refactor */}
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">{t('FirstName')}</Typography>
|
||||
<Typography component="h1">{t("FirstName")}</Typography>
|
||||
</Box>
|
||||
<TextInput
|
||||
id="edit-first-name"
|
||||
@@ -223,7 +220,7 @@ const ProfilePanel = () => {
|
||||
gap={SPACING_GAP}
|
||||
>
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">{t('LastName')}</Typography>
|
||||
<Typography component="h1">{t("LastName")}</Typography>
|
||||
</Box>
|
||||
<TextInput
|
||||
id="edit-last-name"
|
||||
@@ -241,12 +238,12 @@ const ProfilePanel = () => {
|
||||
gap={SPACING_GAP}
|
||||
>
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">{t('email')}</Typography>
|
||||
<Typography component="h1">{t("auth.common.inputs.email.label")}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{t('EmailDescriptionText')}
|
||||
{t("EmailDescriptionText")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<TextInput
|
||||
@@ -264,12 +261,12 @@ const ProfilePanel = () => {
|
||||
gap={SPACING_GAP}
|
||||
>
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">{t('YourPhoto')}</Typography>
|
||||
<Typography component="h1">{t("YourPhoto")}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{t('PhotoDescriptionText')}
|
||||
{t("PhotoDescriptionText")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
@@ -293,14 +290,14 @@ const ProfilePanel = () => {
|
||||
color="error"
|
||||
onClick={handleDeletePicture}
|
||||
>
|
||||
{t('delete')}
|
||||
{t("delete")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained" // CAIO_REVIEW
|
||||
color="accent"
|
||||
onClick={openPictureModal}
|
||||
>
|
||||
{t('update')}
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -325,7 +322,7 @@ const ProfilePanel = () => {
|
||||
disabled={Object.keys(errors).length !== 0 && !errors?.picture && true}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
{t('save')}
|
||||
{t("save")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -344,12 +341,12 @@ const ProfilePanel = () => {
|
||||
spellCheck="false"
|
||||
>
|
||||
<Box mb={theme.spacing(6)}>
|
||||
<Typography component="h1">{t('DeleteAccountTitle')}</Typography>
|
||||
<Typography component="h1">{t("DeleteAccountTitle")}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
>
|
||||
{t('DeleteDescriptionText')}
|
||||
{t("DeleteDescriptionText")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -357,17 +354,17 @@ const ProfilePanel = () => {
|
||||
color="error"
|
||||
onClick={() => setIsOpen("delete")}
|
||||
>
|
||||
{t('DeleteAccountButton')}
|
||||
{t("DeleteAccountButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Dialog
|
||||
open={isModalOpen("delete")}
|
||||
theme={theme}
|
||||
title={t('DeleteWarningTitle')}
|
||||
description={t('DeleteAccountWarning')}
|
||||
title={t("DeleteWarningTitle")}
|
||||
description={t("DeleteAccountWarning")}
|
||||
onCancel={() => setIsOpen("")}
|
||||
confirmationButtonLabel={t('DeleteAccountButton')}
|
||||
confirmationButtonLabel={t("DeleteAccountButton")}
|
||||
onConfirm={handleDeleteAccount}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
@@ -389,13 +386,13 @@ const ProfilePanel = () => {
|
||||
: user?.avatarImage
|
||||
? `data:image/png;base64,${user.avatarImage}`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
onChange={(newFile) => {
|
||||
if (newFile) {
|
||||
setFile(newFile);
|
||||
clearError("unchanged");
|
||||
}
|
||||
}}
|
||||
}}
|
||||
previewIsRound
|
||||
maxSize={3 * 1024 * 1024}
|
||||
/>
|
||||
@@ -410,7 +407,7 @@ const ProfilePanel = () => {
|
||||
color="info"
|
||||
onClick={removePicture}
|
||||
>
|
||||
{t('remove')}
|
||||
{t("remove")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -418,7 +415,7 @@ const ProfilePanel = () => {
|
||||
onClick={handleUpdatePicture}
|
||||
disabled={!!errors.picture || !file?.src}
|
||||
>
|
||||
{t('update')}
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</GenericDialog>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Select from "../../Inputs/Select";
|
||||
@@ -48,7 +48,8 @@ const TeamPanel = () => {
|
||||
{row.firstName + " " + row.lastName}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()}
|
||||
{t("teamPanel.table.created")}{" "}
|
||||
{new Date(row.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -114,7 +115,10 @@ const TeamPanel = () => {
|
||||
email: newEmail,
|
||||
}));
|
||||
|
||||
const validation = credentials.validate({ email: newEmail }, { abortEarly: false });
|
||||
const validation = newOrChangedCredentials.validate(
|
||||
{ email: newEmail },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
@@ -141,7 +145,7 @@ const TeamPanel = () => {
|
||||
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
|
||||
setToInvite((prev) => ({ ...prev, role: ["user"] }));
|
||||
|
||||
const { error } = credentials.validate(
|
||||
const { error } = newOrChangedCredentials.validate(
|
||||
{ email: toInvite.email },
|
||||
{
|
||||
abortEarly: false,
|
||||
@@ -164,7 +168,7 @@ const TeamPanel = () => {
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Unknown error.",
|
||||
body: error?.response?.data?.msg || error.message || "Unknown error.",
|
||||
});
|
||||
} finally {
|
||||
setIsSendingInvite(false);
|
||||
@@ -264,7 +268,7 @@ const TeamPanel = () => {
|
||||
value={toInvite.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
helperText={t(errors.email)}
|
||||
/>
|
||||
<Select
|
||||
id="team-member-role"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const createHeaderFactory = (getCellSx = () => {}) => {
|
||||
return ({ id, content, onClick = () => {}, render = () => {} }) => {
|
||||
return {
|
||||
id,
|
||||
content,
|
||||
onClick,
|
||||
getCellSx,
|
||||
render,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Link from "@mui/material/Link";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
const TextLink = ({ text, linkText, href, target = "_self" }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Typography>{text}</Typography>
|
||||
<Link
|
||||
color="accent"
|
||||
to={href}
|
||||
component={RouterLink}
|
||||
target={target}
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
TextLink.propTypes = {
|
||||
text: PropTypes.string,
|
||||
linkText: PropTypes.string,
|
||||
href: PropTypes.string,
|
||||
target: PropTypes.string,
|
||||
};
|
||||
|
||||
export default TextLink;
|
||||
@@ -14,10 +14,12 @@ import SunAndMoonIcon from "./SunAndMoonIcon";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setMode } from "../../Features/UI/uiSlice";
|
||||
import "./index.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const ThemeSwitch = ({ width = 48, height = 48, color }) => {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleTheme = () => {
|
||||
dispatch(setMode(mode === "light" ? "dark" : "light"));
|
||||
@@ -26,7 +28,7 @@ const ThemeSwitch = ({ width = 48, height = 48, color }) => {
|
||||
return (
|
||||
<IconButton
|
||||
id="theme-toggle"
|
||||
title="Toggles light & dark"
|
||||
title={t("common.buttons.toggleTheme")}
|
||||
className={`theme-${mode}`}
|
||||
aria-label="auto"
|
||||
aria-live="polite"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.wallet-adapter-dropdown {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--env-var-spacing-1);
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--env-var-spacing-1);
|
||||
}
|
||||
|
||||
.wallet-adapter-button {
|
||||
@@ -15,37 +15,39 @@
|
||||
height: var(--env-var-height-2) !important;
|
||||
font-size: var(--env-var-font-size-medium-plus) !important;
|
||||
font-weight: 500 !important;
|
||||
margin: 0;
|
||||
padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2) var(--env-var-spacing-1);
|
||||
margin: 0;
|
||||
padding: calc(
|
||||
(var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2
|
||||
)
|
||||
var(--env-var-spacing-1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wallet-adapter-modal-title {
|
||||
font-size: var(--font-size-h5) !important;
|
||||
font-size: var(--font-size-h5) !important;
|
||||
}
|
||||
|
||||
/* Separator styling */
|
||||
.wallet-adapter-modal-divider {
|
||||
background-color: var(--border-color) !important;
|
||||
margin: var(--spacing-md) 0 !important;
|
||||
background-color: var(--border-color) !important;
|
||||
margin: var(--spacing-md) 0 !important;
|
||||
}
|
||||
|
||||
/* Responsive fixes */
|
||||
@media (max-width: 1200px) {
|
||||
@media (max-width: 1200px) {
|
||||
.wallet-adapter-button {
|
||||
font-size: var(--env-var-font-size-medium) !important;
|
||||
padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium) * 1.2) / 2)
|
||||
var(--env-var-spacing-1-minus) !important;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.wallet-adapter-modal-wrapper {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.wallet-adapter-modal-divider {
|
||||
margin: var(--spacing-sm) 0 !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.wallet-adapter-modal-wrapper {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.wallet-adapter-modal-divider {
|
||||
margin: var(--spacing-sm) 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/createMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitor } = data;
|
||||
const res = await networkService.createMonitor({ monitor: monitor });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const checkInfrastructureEndpointResolution = createAsyncThunk(
|
||||
"infrastructureMonitors/CheckEndpoint",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorURL } = data;
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
monitorURL: monitorURL,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getInfrastructureMonitorById = createAsyncThunk(
|
||||
"infrastructureMonitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorId } = data;
|
||||
const res = await networkService.getMonitorById({ monitorId: monitorId });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getInfrastructureMonitorsByTeamId = createAsyncThunk(
|
||||
"infrastructureMonitors/getMonitorsByTeamId",
|
||||
async (_, thunkApi) => {
|
||||
const user = thunkApi.getState().auth.user;
|
||||
try {
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
teamId: user.teamId,
|
||||
types: ["hardware"],
|
||||
limit: 1,
|
||||
rowsPerPage: 0,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/updateMonitor",
|
||||
async ({ monitorId, monitor }, thunkApi) => {
|
||||
try {
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
thresholds: monitor.thresholds,
|
||||
secret: monitor.secret,
|
||||
};
|
||||
const res = await networkService.updateMonitor({
|
||||
monitorId,
|
||||
monitor,
|
||||
updatedFields,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitor } = data;
|
||||
const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const pauseInfrastructureMonitor = createAsyncThunk(
|
||||
"infrastructureMonitors/pauseMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorId } = data;
|
||||
const res = await networkService.pauseMonitorById({ monitorId: monitorId });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteInfrastructureMonitorChecksByTeamId = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteChecksByTeamId",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { teamId } = data;
|
||||
const res = await networkService.deleteChecksByTeamId({ teamId: teamId });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAllInfrastructureMonitors = createAsyncThunk(
|
||||
"infrastructureMonitors/deleteAllMonitors",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.deleteAllMonitors();
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const infrastructureMonitorsSlice = createSlice({
|
||||
name: "infrastructureMonitors",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearInfrastructureMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getInfrastructureMonitorsByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorsByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorsByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting infrastructure monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkInfrastructureEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkInfrastructureEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkInfrastructureEndpointResolution.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to check endpoint resolution";
|
||||
})
|
||||
// *****************************************************
|
||||
// Get Monitor By Id
|
||||
// *****************************************************
|
||||
.addCase(getInfrastructureMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getInfrastructureMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to get infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updateInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updateInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update infrastructure monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deleteInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete Monitor checks by Team ID
|
||||
// *****************************************************
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteInfrastructureMonitorChecksByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete monitor checks";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pauseInfrastructureMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pauseInfrastructureMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pauseInfrastructureMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause infrastructure monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete all Monitors
|
||||
// *****************************************************
|
||||
.addCase(deleteAllInfrastructureMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteAllInfrastructureMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteAllInfrastructureMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setInfrastructureMonitors, clearInfrastructureMonitorState } =
|
||||
infrastructureMonitorsSlice.actions;
|
||||
|
||||
export default infrastructureMonitorsSlice.reducer;
|
||||
@@ -1,309 +0,0 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createPageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/createPageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitor } = data;
|
||||
const res = await networkService.createMonitor({ monitor: monitor });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const checkEndpointResolution = createAsyncThunk(
|
||||
"monitors/checkEndpoint",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorURL } = data;
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
monitorURL: monitorURL,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getPagespeedMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorId } = data;
|
||||
const res = await networkService.getMonitorById({ monitorId: monitorId });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getPageSpeedByTeamId = createAsyncThunk(
|
||||
"pageSpeedMonitors/getPageSpeedByTeamId",
|
||||
async (_, thunkApi) => {
|
||||
const user = thunkApi.getState().auth.user;
|
||||
try {
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId({
|
||||
teamId: user.teamId,
|
||||
types: ["pagespeed"],
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updatePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/updatePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const res = await networkService.updateMonitor({
|
||||
monitorId: monitor._id,
|
||||
updatedFields: updatedFields,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deletePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/deletePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitor } = data;
|
||||
const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
export const pausePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/pausePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorId } = data;
|
||||
const res = await networkService.pauseMonitorById({ monitorId: monitorId });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const pageSpeedMonitorSlice = createSlice({
|
||||
name: "pageSpeedMonitor",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getPageSpeedByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPageSpeedByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getPageSpeedByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting page speed monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
.addCase(getPagespeedMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to get pagespeed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createPageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createPageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createPageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create page speed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkEndpointResolution.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to check endpoint resolution";
|
||||
})
|
||||
// *****************************************************
|
||||
// Update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updatePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updatePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updatePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update page speed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deletePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deletePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deletePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete page speed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pausePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pausePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pausePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause page speed monitor";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMonitors, clearMonitorState } = pageSpeedMonitorSlice.actions;
|
||||
|
||||
export default pageSpeedMonitorSlice.reducer;
|
||||
@@ -1,101 +0,0 @@
|
||||
import { networkService } from "../../main";
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
language: "gb",
|
||||
pagespeedApiKey: "",
|
||||
};
|
||||
|
||||
export const getAppSettings = createAsyncThunk(
|
||||
"settings/getSettings",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.getAppSettings();
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateAppSettings = createAsyncThunk(
|
||||
"settings/updateSettings",
|
||||
async ({ settings }, thunkApi) => {
|
||||
try {
|
||||
const parsedSettings = {
|
||||
language: settings.language,
|
||||
pagespeedApiKey: settings.pagespeedApiKey,
|
||||
};
|
||||
const res = await networkService.updateAppSettings({ settings: parsedSettings });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleGetSettingsFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
state.language = action.payload.data.language;
|
||||
state.pagespeedApiKey = action.payload.data.pagespeedApiKey;
|
||||
};
|
||||
const handleGetSettingsRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to get settings.";
|
||||
};
|
||||
const handleUpdateSettingsFulfilled = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
};
|
||||
const handleUpdateSettingsRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to update settings.";
|
||||
};
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: "settings",
|
||||
initialState,
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(getAppSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getAppSettings.fulfilled, handleGetSettingsFulfilled)
|
||||
.addCase(getAppSettings.rejected, handleGetSettingsRejected);
|
||||
|
||||
builder
|
||||
.addCase(updateAppSettings.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateAppSettings.fulfilled, handleUpdateSettingsFulfilled)
|
||||
.addCase(updateAppSettings.rejected, handleUpdateSettingsRejected);
|
||||
},
|
||||
});
|
||||
|
||||
export default settingsSlice.reducer;
|
||||
@@ -16,14 +16,18 @@ const initialState = {
|
||||
maintenance: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
infrastructure: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
sidebar: {
|
||||
collapsed: false,
|
||||
},
|
||||
mode: initialMode,
|
||||
showURL: false,
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
timezone: "America/Toronto",
|
||||
distributedUptimeEnabled: false,
|
||||
language: "gb",
|
||||
language: "en",
|
||||
starPromptOpen: true,
|
||||
};
|
||||
|
||||
@@ -46,6 +50,9 @@ const uiSlice = createSlice({
|
||||
setMode: (state, action) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
setShowURL: (state, action) => {
|
||||
state.showURL = action.payload;
|
||||
},
|
||||
setGreeting(state, action) {
|
||||
state.greeting.index = action.payload.index;
|
||||
state.greeting.lastUpdate = action.payload.lastUpdate;
|
||||
@@ -53,7 +60,7 @@ const uiSlice = createSlice({
|
||||
setTimezone(state, action) {
|
||||
state.timezone = action.payload.timezone;
|
||||
},
|
||||
setLanguage(state, action) {
|
||||
setLanguage: (state, action) => {
|
||||
state.language = action.payload;
|
||||
},
|
||||
setStarPromptOpen: (state, action) => {
|
||||
@@ -67,6 +74,7 @@ export const {
|
||||
setRowsPerPage,
|
||||
toggleSidebar,
|
||||
setMode,
|
||||
setShowURL,
|
||||
setGreeting,
|
||||
setTimezone,
|
||||
setDistributedUptimeEnabled,
|
||||
|
||||
@@ -1,380 +0,0 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
|
||||
export const createUptimeMonitor = createAsyncThunk(
|
||||
"monitors/createMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitor } = data;
|
||||
const res = await networkService.createMonitor({ monitor: monitor });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const checkEndpointResolution = createAsyncThunk(
|
||||
"monitors/checkEndpoint",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorURL } = data;
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
monitorURL: monitorURL,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getUptimeMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorId } = data;
|
||||
const res = await networkService.getMonitorById({ monitorId: monitorId });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updateUptimeMonitor = createAsyncThunk(
|
||||
"monitors/updateMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
matchMethod: monitor.matchMethod,
|
||||
expectedValue: monitor.expectedValue,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
jsonPath: monitor.jsonPath,
|
||||
...(monitor.type === "port" && { port: monitor.port }),
|
||||
};
|
||||
const res = await networkService.updateMonitor({
|
||||
monitorId: monitor._id,
|
||||
updatedFields,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteUptimeMonitor = createAsyncThunk(
|
||||
"monitors/deleteMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitor } = data;
|
||||
const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const pauseUptimeMonitor = createAsyncThunk(
|
||||
"monitors/pauseMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { monitorId } = data;
|
||||
const res = await networkService.pauseMonitorById({ monitorId: monitorId });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteMonitorChecksByTeamId = createAsyncThunk(
|
||||
"monitors/deleteChecksByTeamId",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { teamId } = data;
|
||||
const res = await networkService.deleteChecksByTeamId({ teamId: teamId });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
export const addDemoMonitors = createAsyncThunk(
|
||||
"monitors/addDemoMonitors",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.addDemoMonitors();
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAllMonitors = createAsyncThunk(
|
||||
"monitors/deleteAllMonitors",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const res = await networkService.deleteAllMonitors();
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const uptimeMonitorsSlice = createSlice({
|
||||
name: "uptimeMonitors",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearUptimeMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
.addCase(createUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(createUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(createUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkEndpointResolution.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to check endpoint resolution";
|
||||
})
|
||||
// *****************************************************
|
||||
// Get Monitor By Id
|
||||
// *****************************************************
|
||||
.addCase(getUptimeMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getUptimeMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getUptimeMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to get uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updateUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updateUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update uptime monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deleteUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete Monitor checks by Team ID
|
||||
// *****************************************************
|
||||
.addCase(deleteMonitorChecksByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete monitor checks";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pauseUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pauseUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pauseUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Add Demo Monitors
|
||||
// *****************************************************
|
||||
.addCase(addDemoMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(addDemoMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(addDemoMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to add demo uptime monitors";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete all Monitors
|
||||
// *****************************************************
|
||||
.addCase(deleteAllMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteAllMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteAllMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setUptimeMonitors, clearUptimeMonitorState } = uptimeMonitorsSlice.actions;
|
||||
|
||||
export default uptimeMonitorsSlice.reducer;
|
||||
@@ -0,0 +1,213 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const useFetchChecksTeam = ({
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
ack,
|
||||
page,
|
||||
rowsPerPage,
|
||||
enabled = true,
|
||||
updateTrigger,
|
||||
}) => {
|
||||
const [checks, setChecks] = useState(undefined);
|
||||
const [checksCount, setChecksCount] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChecks = async () => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
ack,
|
||||
page,
|
||||
rowsPerPage,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getChecksByTeam(config);
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChecks();
|
||||
}, [
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
ack,
|
||||
page,
|
||||
rowsPerPage,
|
||||
enabled,
|
||||
updateTrigger,
|
||||
]);
|
||||
|
||||
return [checks, checksCount, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchChecksByMonitor = ({
|
||||
monitorId,
|
||||
type,
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
ack,
|
||||
page,
|
||||
rowsPerPage,
|
||||
enabled = true,
|
||||
updateTrigger,
|
||||
}) => {
|
||||
const [checks, setChecks] = useState(undefined);
|
||||
const [checksCount, setChecksCount] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChecks = async () => {
|
||||
if (!enabled || !type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
monitorId,
|
||||
type,
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
ack,
|
||||
page,
|
||||
rowsPerPage,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getChecksByMonitor(config);
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChecks();
|
||||
}, [
|
||||
monitorId,
|
||||
type,
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
ack,
|
||||
page,
|
||||
rowsPerPage,
|
||||
enabled,
|
||||
updateTrigger,
|
||||
]);
|
||||
|
||||
return [checks, checksCount, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchChecksSummaryByTeamId = ({ updateTrigger } = {}) => {
|
||||
const [summary, setSummary] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSummary = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const res = await networkService.getChecksAndSummaryByTeamId();
|
||||
setSummary(res.data.data);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSummary();
|
||||
}, [updateTrigger]);
|
||||
|
||||
return [summary, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useResolveIncident = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resolveIncident = async (checkId, setUpdateTrigger) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.updateCheckStatus({
|
||||
checkId,
|
||||
ack: true,
|
||||
});
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
} catch (error) {
|
||||
createToast({ body: t("checkHooks.failureResolveOne") });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [resolveIncident, isLoading];
|
||||
};
|
||||
|
||||
const useAckAllChecks = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ackAllChecks = async (setUpdateTrigger) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.updateAllChecksStatus({ ack: true });
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
} catch (error) {
|
||||
createToast({ body: t("checkHooks.failureResolveAll") });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [ackAllChecks, isLoading];
|
||||
};
|
||||
|
||||
export {
|
||||
useFetchChecksByMonitor,
|
||||
useFetchChecksTeam,
|
||||
useFetchChecksSummaryByTeamId,
|
||||
useResolveIncident,
|
||||
useAckAllChecks,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const useFetchLogs = () => {
|
||||
const { t } = useTranslation();
|
||||
const [logs, setLogs] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getLogs();
|
||||
setLogs(response.data.data);
|
||||
createToast({
|
||||
body: t("logsPage.toast.fetchLogsSuccess"),
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
createToast({
|
||||
message: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchLogs();
|
||||
}, [t]);
|
||||
return [logs, isLoading, error];
|
||||
};
|
||||
|
||||
export { useFetchLogs };
|
||||
@@ -0,0 +1,510 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "./useMonitorUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getMonitorsWithSummaryByTeamId({
|
||||
types,
|
||||
});
|
||||
const { monitors, summary } = res?.data?.data ?? {};
|
||||
setMonitors(monitors);
|
||||
setMonitorsSummary(summary);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [types, monitorUpdateTrigger]);
|
||||
return [monitors, monitorsSummary, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchMonitorsWithChecks = ({
|
||||
types,
|
||||
limit,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
monitorUpdateTrigger,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [count, setCount] = useState(undefined);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
const { getMonitorWithPercentage } = useMonitorUtils();
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getMonitorsWithChecksByTeamId({
|
||||
limit,
|
||||
types,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
});
|
||||
const { count, monitors } = res?.data?.data ?? {};
|
||||
const mappedMonitors = monitors.map((monitor) =>
|
||||
getMonitorWithPercentage(monitor, theme)
|
||||
);
|
||||
setMonitors(mappedMonitors);
|
||||
setCount(count?.monitorsCount ?? 0);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [
|
||||
field,
|
||||
filter,
|
||||
getMonitorWithPercentage,
|
||||
limit,
|
||||
order,
|
||||
page,
|
||||
rowsPerPage,
|
||||
theme,
|
||||
types,
|
||||
monitorUpdateTrigger,
|
||||
]);
|
||||
return [monitors, count, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchMonitorsByTeamId = ({
|
||||
types,
|
||||
limit,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
checkOrder,
|
||||
normalize,
|
||||
status,
|
||||
updateTrigger,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [summary, setSummary] = useState(undefined);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
limit,
|
||||
types,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
checkOrder,
|
||||
status,
|
||||
normalize,
|
||||
});
|
||||
if (res?.data?.data?.filteredMonitors) {
|
||||
setMonitors(res.data.data.filteredMonitors);
|
||||
setSummary(res.data.data.summary);
|
||||
}
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [
|
||||
types,
|
||||
limit,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
updateTrigger,
|
||||
checkOrder,
|
||||
normalize,
|
||||
status,
|
||||
]);
|
||||
return [monitors, summary, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchStatsByMonitorId = ({
|
||||
monitorId,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
updateTrigger,
|
||||
}) => {
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
const [audits, setAudits] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getStatsByMonitorId({
|
||||
monitorId: monitorId,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
});
|
||||
setMonitor(res?.data?.data ?? undefined);
|
||||
setAudits(res?.data?.data?.checks?.[0]?.audits ?? undefined);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, dateRange, numToDisplay, normalize, sortOrder, limit, updateTrigger]);
|
||||
return [monitor, audits, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
if (typeof monitorId === "undefined") {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getMonitorById({ monitorId: monitorId });
|
||||
setMonitor(res.data.data);
|
||||
} catch (error) {
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, setMonitor, updateTrigger]);
|
||||
return [isLoading];
|
||||
};
|
||||
|
||||
const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
if (!monitorId) {
|
||||
return { monitor: undefined, isLoading: false, networkError: undefined };
|
||||
}
|
||||
const response = await networkService.getHardwareDetailsByMonitorId({
|
||||
monitorId: monitorId,
|
||||
dateRange: dateRange,
|
||||
});
|
||||
setMonitor(response.data.data);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, dateRange, updateTrigger]);
|
||||
return [monitor, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
const [monitorStats, setMonitorStats] = useState(undefined);
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
const res = await networkService.getUptimeDetailsById({
|
||||
monitorId: monitorId,
|
||||
dateRange: dateRange,
|
||||
normalize: true,
|
||||
});
|
||||
const { monitorData, monitorStats } = res?.data?.data ?? {};
|
||||
setMonitor(monitorData);
|
||||
setMonitorStats(monitorStats);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [dateRange, monitorId, trigger]);
|
||||
return [monitor, monitorStats, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useCreateMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const createMonitor = async ({ monitor, redirect }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.createMonitor({ monitor });
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
if (redirect) {
|
||||
navigate(redirect);
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return [createMonitor, isLoading];
|
||||
};
|
||||
|
||||
const useDeleteMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const deleteMonitor = async ({ monitor, redirect }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.deleteMonitorById({ monitorId: monitor._id });
|
||||
createToast({ body: "Monitor deleted successfully!" });
|
||||
if (redirect) {
|
||||
navigate(redirect);
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return [deleteMonitor, isLoading];
|
||||
};
|
||||
|
||||
const useUpdateMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const updateMonitor = async ({ monitor, redirect }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
matchMethod: monitor.matchMethod,
|
||||
expectedValue: monitor.expectedValue,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
jsonPath: monitor.jsonPath,
|
||||
...(monitor.type === "port" && { port: monitor.port }),
|
||||
...(monitor.type === "hardware" && {
|
||||
thresholds: monitor.thresholds,
|
||||
secret: monitor.secret,
|
||||
}),
|
||||
};
|
||||
await networkService.updateMonitor({
|
||||
monitorId: monitor._id,
|
||||
updatedFields,
|
||||
});
|
||||
|
||||
createToast({ body: "Monitor updated successfully!" });
|
||||
if (redirect) {
|
||||
navigate(redirect);
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to update monitor." });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return [updateMonitor, isLoading];
|
||||
};
|
||||
|
||||
const usePauseMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
const pauseMonitor = async ({ monitorId, triggerUpdate }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.pauseMonitorById({ monitorId });
|
||||
createToast({
|
||||
body: res.data.data.isActive
|
||||
? "Monitor resumed successfully"
|
||||
: "Monitor paused successfully",
|
||||
});
|
||||
triggerUpdate();
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [pauseMonitor, isLoading, error];
|
||||
};
|
||||
|
||||
const useAddDemoMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const addDemoMonitors = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.addDemoMonitors();
|
||||
createToast({ body: t("monitorHooks.successAddDemoMonitors") });
|
||||
} catch (error) {
|
||||
createToast({ body: t("monitorHooks.failureAddDemoMonitors") });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return [addDemoMonitors, isLoading];
|
||||
};
|
||||
|
||||
const useDeleteAllMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const deleteAllMonitors = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.deleteAllMonitors();
|
||||
createToast({ body: t("settingsMonitorsDeleted") });
|
||||
} catch (error) {
|
||||
createToast({ body: t("settingsFailedToDeleteMonitors") });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return [deleteAllMonitors, isLoading];
|
||||
};
|
||||
|
||||
const useDeleteMonitorStats = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const deleteMonitorStats = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await networkService.deleteChecksByTeamId();
|
||||
createToast({ body: t("settingsStatsCleared") });
|
||||
} catch (error) {
|
||||
createToast({ body: t("settingsFailedToClearStats") });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [deleteMonitorStats, isLoading];
|
||||
};
|
||||
|
||||
const useCreateBulkMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const createBulkMonitors = async (file, user) => {
|
||||
setIsLoading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("csvFile", file);
|
||||
|
||||
try {
|
||||
const response = await networkService.createBulkMonitors(formData);
|
||||
return [true, response.data, null]; // [success, data, error]
|
||||
} catch (err) {
|
||||
const errorMessage = err?.response?.data?.msg || err.message;
|
||||
return [false, null, errorMessage];
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [createBulkMonitors, isLoading];
|
||||
};
|
||||
|
||||
const useExportMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const exportMonitors = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await networkService.exportMonitors();
|
||||
|
||||
// Create a download link
|
||||
const url = window.URL.createObjectURL(response.data);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "monitors.csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
createToast({ body: t("export.success") });
|
||||
return [true, null];
|
||||
} catch (err) {
|
||||
const errorMessage = err?.response?.data?.msg || err.message;
|
||||
createToast({ body: errorMessage || t("export.failed") });
|
||||
return [false, errorMessage];
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [exportMonitors, isLoading];
|
||||
};
|
||||
|
||||
export {
|
||||
useFetchMonitorsWithSummary,
|
||||
useFetchMonitorsWithChecks,
|
||||
useFetchMonitorsByTeamId,
|
||||
useFetchStatsByMonitorId,
|
||||
useFetchMonitorById,
|
||||
useFetchUptimeMonitorById,
|
||||
useFetchHardwareMonitorById,
|
||||
useCreateMonitor,
|
||||
useDeleteMonitor,
|
||||
useUpdateMonitor,
|
||||
usePauseMonitor,
|
||||
useAddDemoMonitors,
|
||||
useDeleteAllMonitors,
|
||||
useDeleteMonitorStats,
|
||||
useCreateBulkMonitors,
|
||||
useExportMonitors,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
|
||||
const useFetchQueueData = (trigger) => {
|
||||
const [jobs, setJobs] = useState(undefined);
|
||||
const [metrics, setMetrics] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchJobs = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getQueueData();
|
||||
if (response.status === 200) {
|
||||
setJobs(response.data.data.jobs);
|
||||
setMetrics(response.data.data.metrics);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchJobs();
|
||||
}, [trigger]);
|
||||
|
||||
return [jobs, metrics, isLoading, error];
|
||||
};
|
||||
|
||||
const useFlushQueue = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
|
||||
const flushQueue = async (trigger, setTrigger) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.flushQueue();
|
||||
createToast({
|
||||
body: "Queue flushed",
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setTrigger(!trigger);
|
||||
}
|
||||
};
|
||||
return [flushQueue, isLoading, error];
|
||||
};
|
||||
|
||||
export { useFetchQueueData, useFlushQueue };
|
||||
Loaded 100 of 364 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user