Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a924227805 | ||
|
|
e4e75632c5 | ||
|
|
ddb7c24832 | ||
|
|
2144d3f8c0 | ||
|
|
4a824dec81 | ||
|
|
b35fee1be8 | ||
|
|
1b167af12b | ||
|
|
f2962e34ae | ||
|
|
386db5029a | ||
|
|
0e6e54a124 | ||
|
|
c666cb3879 | ||
|
|
4039517683 | ||
|
|
00fc7b8038 | ||
|
|
81d283ec8d | ||
|
|
105100e349 | ||
|
|
4598690dbf | ||
|
|
f68341c315 | ||
|
|
e8a8cea909 | ||
|
|
bf53f107e3 | ||
|
|
00de974528 | ||
|
|
0445398e94 | ||
|
|
272e2ba955 | ||
|
|
0a2adc2d92 | ||
|
|
7008fc1770 | ||
|
|
041a261540 | ||
|
|
1008d819f7 | ||
|
|
fb84a79a04 | ||
|
|
fcd6786149 | ||
|
|
45bfdbdce0 | ||
|
|
1537c2141f | ||
|
|
ea968ebf3c | ||
|
|
3ba1a52644 | ||
|
|
a015cc6706 | ||
|
|
5bad362793 | ||
|
|
5520756edd | ||
|
|
ba954a1d6e | ||
|
|
2944ff298c | ||
|
|
80d121e87a | ||
|
|
8a7b852974 | ||
|
|
ed8352d857 | ||
|
|
41f64ca9fe | ||
|
|
738e49dc92 | ||
|
|
68a16257c6 | ||
|
|
5dcbae55f4 | ||
|
|
e0d963256c | ||
|
|
2bff4bbd15 | ||
|
|
a0eba89f34 | ||
|
|
4e78de9da2 | ||
|
|
85f00eb7e8 | ||
|
|
8e13855b66 | ||
|
|
06f49e24c2 | ||
|
|
e598ae45f7 | ||
|
|
ad7f469e06 | ||
|
|
c8d3e3b70c | ||
|
|
cb57ff5cc2 | ||
|
|
72f070fbcb | ||
|
|
fb0a86d37c | ||
|
|
36402dfefa | ||
|
|
c61874dbdd | ||
|
|
20848f28d5 | ||
|
|
5d23966f00 | ||
|
|
294cc66e4a | ||
|
|
0fd2e7032a | ||
|
|
340b85043e | ||
|
|
1821ae3c2b | ||
|
|
c536cb2afa | ||
|
|
3f79b4c2f6 | ||
|
|
137ba9185e | ||
|
|
2f6a0913dc | ||
|
|
2c7336d440 | ||
|
|
5a022ff3f6 | ||
|
|
fe4855500e | ||
|
|
ce8f5cbe0c | ||
|
|
7a90a6f9f4 | ||
|
|
b6bebe33fe | ||
|
|
9cf8072c1e | ||
|
|
0ca00ca6a1 | ||
|
|
619c5242db | ||
|
|
ff07854590 | ||
|
|
ba99989c66 | ||
|
|
3c75933cf2 | ||
|
|
ea9ef7afd3 | ||
|
|
f5e633a781 | ||
|
|
90b94816dd | ||
|
|
11dcaef3ff | ||
|
|
3f3e7f71ec | ||
|
|
b6070572cf | ||
|
|
e818a02a2a | ||
|
|
d8c8e222c8 | ||
|
|
f12b9715bc | ||
|
|
31c65d3e0b | ||
|
|
fcd6741cf9 | ||
|
|
d663b9230f | ||
|
|
5f2d61ad7a | ||
|
|
ec80a2dc0b | ||
|
|
2cece3de59 | ||
|
|
79eb580802 | ||
|
|
b93e3327a7 | ||
|
|
f01030a47a | ||
|
|
3cbd907c09 | ||
|
|
8fae69b7a4 | ||
|
|
e45dd4240e | ||
|
|
16a8552e6c | ||
|
|
c953ec6152 | ||
|
|
a433f10240 | ||
|
|
b73b97b5e9 | ||
|
|
0607e0b228 | ||
|
|
3bd4f67ec7 | ||
|
|
845d5a5d51 | ||
|
|
0101da731f | ||
|
|
68592acd39 | ||
|
|
b86fc5edb3 | ||
|
|
32b54ddf11 | ||
|
|
f8a0741e6c | ||
|
|
652d231771 | ||
|
|
1c3c5f4fec | ||
|
|
e7006a4a44 | ||
|
|
0f81d54f25 | ||
|
|
f3121c24c6 | ||
|
|
7abd0f2cd4 | ||
|
|
46b342005a | ||
|
|
4e0349bd58 | ||
|
|
30a569233b | ||
|
|
44ba6b4d0f | ||
|
|
9065070eb2 | ||
|
|
fd951bfdd5 | ||
|
|
27d613b594 | ||
|
|
ba757b9aa4 | ||
|
|
c445463c0c | ||
|
|
4693177e4b | ||
|
|
30b135eba3 | ||
|
|
bda48e07f1 | ||
|
|
7c8ab64e34 | ||
|
|
ec7855bd02 | ||
|
|
d401eefd6b | ||
|
|
0ad759a384 | ||
|
|
d3d20b29c3 | ||
|
|
fbb570c0d5 | ||
|
|
2318082bb8 | ||
|
|
05dd5f7de2 | ||
|
|
841251d09e | ||
|
|
a4dd8f70f0 | ||
|
|
9d0d1329b6 | ||
|
|
7a929777b4 | ||
|
|
ce3ec99c9f | ||
|
|
4f7847275e | ||
|
|
9a5ddb22b3 | ||
|
|
b1322cbec3 | ||
|
|
637bc84136 | ||
|
|
3147f02b6a | ||
|
|
dabff74946 | ||
|
|
de99577b9d | ||
|
|
1846432601 | ||
|
|
6f307dc822 | ||
|
|
f43210e29e | ||
|
|
50b9be4eca | ||
|
|
1a38222974 | ||
|
|
cb9cd0a43c | ||
|
|
c2e2ae8dbd | ||
|
|
df790a92be | ||
|
|
e1f9242b34 | ||
|
|
9d2a52e639 | ||
|
|
5cbca9457e | ||
|
|
25c66cff43 | ||
|
|
75a2e0deee | ||
|
|
b857137f1a | ||
|
|
40141cdcb0 | ||
|
|
c5c1d6903e | ||
|
|
f15c8188b9 | ||
|
|
8f971cfb02 | ||
|
|
32c811ec5a | ||
|
|
4febc56c30 | ||
|
|
f22ee4a68b | ||
|
|
87bec0399f | ||
|
|
1f29255dd1 | ||
|
|
4a11638182 | ||
|
|
10743372f8 | ||
|
|
d4b11bbb76 | ||
|
|
ced2417c98 | ||
|
|
27415aeca6 | ||
|
|
9bd2e336a6 | ||
|
|
d5778ec173 | ||
|
|
74e80f086a | ||
|
|
b1c6191fa1 | ||
|
|
8e0245c844 | ||
|
|
613fbf8c39 | ||
|
|
1788cda5c2 | ||
|
|
155adf549e | ||
|
|
5463542532 | ||
|
|
f5a25bc0e5 | ||
|
|
993339d0d1 | ||
|
|
bc4fe897cf | ||
|
|
c773bdaabd | ||
|
|
9f9a2be1d8 | ||
|
|
1e4b0c2f20 | ||
|
|
56025f6d56 | ||
|
|
5188b90f32 | ||
|
|
7178f537a8 | ||
|
|
45ac589975 | ||
|
|
9925e10ffc | ||
|
|
e3fde8d564 | ||
|
|
8aee01e008 | ||
|
|
cd203ff067 | ||
|
|
254cb6d80f | ||
|
|
9b3ad54d0a | ||
|
|
4ae3bb5780 | ||
|
|
b8aa1ef765 | ||
|
|
5e5daef49a | ||
|
|
88c478c533 | ||
|
|
1ecec115e4 | ||
|
|
8ff597eedb | ||
|
|
69f74488e5 | ||
|
|
ded9a27f42 | ||
|
|
43284ae625 | ||
|
|
f7b1ef9741 | ||
|
|
a6d445818f | ||
|
|
122a84b9e2 | ||
|
|
065a6cb1fd | ||
|
|
ae9eb494ae | ||
|
|
0241797e79 | ||
|
|
9d45947b08 | ||
|
|
f2c26c0eed | ||
|
|
f7a1416070 | ||
|
|
9e05d024a8 | ||
|
|
3bd4c8d747 | ||
|
|
9ae7314f54 | ||
|
|
c77d1e3322 | ||
|
|
0e767d6581 | ||
|
|
7ff5935f94 | ||
|
|
cb4566f829 | ||
|
|
52249d5b7e | ||
|
|
22e0507fbd | ||
|
|
e32e72de7a | ||
|
|
ff31ee773f | ||
|
|
a1cbbf5d42 | ||
|
|
ce667bc53b | ||
|
|
d5526b89d9 | ||
|
|
096cfccf40 | ||
|
|
f381b0ce4a | ||
|
|
867b530573 | ||
|
|
2141c3eacd | ||
|
|
13a5f699ba | ||
|
|
7ab5f4ab09 | ||
|
|
480ba034ce | ||
|
|
d560e923d2 | ||
|
|
235fc979b2 | ||
|
|
f5aefb1f0b | ||
|
|
e27cc8a12b | ||
|
|
f2e2d5c2ed | ||
|
|
6d999eccbd | ||
|
|
136144b8ec | ||
|
|
355a445ead | ||
|
|
c490508252 | ||
|
|
7b4160ac92 | ||
|
|
781854d85c | ||
|
|
86dc71687d | ||
|
|
f0e79121a7 | ||
|
|
ed1b88f1fd | ||
|
|
bb5a3e73c8 | ||
|
|
38b2025286 | ||
|
|
c12f5880e2 | ||
|
|
94cf22ab8e | ||
|
|
0bc8142e71 | ||
|
|
d890b972be | ||
|
|
05945a9a74 | ||
|
|
88b68a9df0 | ||
|
|
8b322a0ec0 | ||
|
|
7968ed8a19 | ||
|
|
2fba22d0f6 | ||
|
|
cce930b90a | ||
|
|
c5dd16511d | ||
|
|
65f437f08e | ||
|
|
47c231a214 | ||
|
|
f5d0b74ccf | ||
|
|
17cac37967 | ||
|
|
b7e7028b6f | ||
|
|
ea3df7be42 | ||
|
|
f0d7e82e22 | ||
|
|
c6d56b30e4 | ||
|
|
8225924da6 | ||
|
|
6517e9ee6c | ||
|
|
10ad3397a3 | ||
|
|
dd9468cb2e | ||
|
|
0b9ab0614f | ||
|
|
e13f495d71 | ||
|
|
029b8fac37 | ||
|
|
6e5ada79de | ||
|
|
59f050135a | ||
|
|
dc8b0aed9f | ||
|
|
424df24bf3 | ||
|
|
d451f725cd | ||
|
|
d04b8fddb0 | ||
|
|
bb592964ee | ||
|
|
786dd427bc | ||
|
|
4c2c452b91 | ||
|
|
1765bf7e55 | ||
|
|
e19de23770 | ||
|
|
765a1a24ed | ||
|
|
e42b4b2808 | ||
|
|
65f6925864 | ||
|
|
02b7dc080b | ||
|
|
d4e8381827 | ||
|
|
de34710c90 | ||
|
|
cd8752c1cb | ||
|
|
86a0b0b60c | ||
|
|
fc5ccf1cf8 | ||
|
|
45352b62e1 | ||
|
|
0ea3379397 | ||
|
|
281cbbc30f | ||
|
|
0de9e067c6 | ||
|
|
77c00558aa | ||
|
|
10c5463892 | ||
|
|
7b963ca877 | ||
|
|
aa35e9211b | ||
|
|
2fc0eb9460 | ||
|
|
91b4c976e5 | ||
|
|
950c5fdc92 | ||
|
|
114572024c | ||
|
|
8503b7ece4 | ||
|
|
b53650a50f | ||
|
|
9ad3b58fcd | ||
|
|
521c6988a5 | ||
|
|
7900e1104f | ||
|
|
035edd92f4 | ||
|
|
2c4c6a5024 | ||
|
|
12747bd95b | ||
|
|
7c22663250 | ||
|
|
eb4856901b | ||
|
|
da905169e1 | ||
|
|
fe9bf6375f | ||
|
|
800f988979 | ||
|
|
b4f42bb7d4 | ||
|
|
e58c5fdb86 | ||
|
|
4e3f552538 | ||
|
|
2d01205f70 | ||
|
|
ebfb25ebc0 | ||
|
|
d93fb48202 | ||
|
|
a73d0ef3b4 | ||
|
|
8274d4ffbe | ||
|
|
48990b860f | ||
|
|
b61a7ffc3c | ||
|
|
37a3028540 | ||
|
|
2e675f5dee | ||
|
|
6a2a79c086 | ||
|
|
a86e9ff480 | ||
|
|
3dc332010e | ||
|
|
61a1972c4c | ||
|
|
0779e5ea15 | ||
|
|
fab78f0cee | ||
|
|
ce49270cf8 | ||
|
|
7a190f7c69 | ||
|
|
126767ed5c | ||
|
|
7d5302cfcf | ||
|
|
87d3d6a8f0 | ||
|
|
927e8a88b8 | ||
|
|
73a6277feb | ||
|
|
94a7044b7b | ||
|
|
54d3392cf1 | ||
|
|
f1c9aad4e6 | ||
|
|
48cf8a5e73 | ||
|
|
b5f4c04165 | ||
|
|
a9c2549376 | ||
|
|
7bf96f6194 | ||
|
|
ae40cb8a54 | ||
|
|
16ebe4b1ab | ||
|
|
c7914a1c4f | ||
|
|
6b12246ec0 | ||
|
|
4a42b6eb43 | ||
|
|
de742068a7 | ||
|
|
b383ce5b11 | ||
|
|
ccb424eafe | ||
|
|
585fb4f700 | ||
|
|
00f5eaf44d | ||
|
|
8bacbcde89 | ||
|
|
245e436a88 | ||
|
|
2d6fdcb1e2 | ||
|
|
8adad64248 | ||
|
|
e7cf6d385c | ||
|
|
753a904c94 | ||
|
|
0ccbd17709 | ||
|
|
78a5b2e666 | ||
|
|
5e46a9f2d5 | ||
|
|
0ecbf48f5d | ||
|
|
d4ecb26ffe | ||
|
|
e04ac77ddd | ||
|
|
cd3702f5b5 | ||
|
|
ca02ed9d18 | ||
|
|
f934bbf829 | ||
|
|
da7d9c02c8 | ||
|
|
e55d8618bd | ||
|
|
af010e138d | ||
|
|
9d94b71583 | ||
|
|
38d31192ed | ||
|
|
a0799874e4 | ||
|
|
5ec2500310 | ||
|
|
0bd9aa1e3f | ||
|
|
20dd3ead3e | ||
|
|
b30a512d6e | ||
|
|
e63faebcb8 | ||
|
|
7e5cb1facc | ||
|
|
10c822c5d9 | ||
|
|
8761b3e28d | ||
|
|
19cccbbcad | ||
|
|
dbea93f9ba | ||
|
|
4629014922 | ||
|
|
24e7b1c224 | ||
|
|
ad06544a64 | ||
|
|
8d105e636d | ||
|
|
bf5d7f3c53 | ||
|
|
c5cd3512af | ||
|
|
2802cce6f1 | ||
|
|
6e49e19ba1 | ||
|
|
0c6d635b67 | ||
|
|
de80a530e5 | ||
|
|
63b106d844 | ||
|
|
ab96e321c9 | ||
|
|
0b7d1abb71 | ||
|
|
592c184ffe | ||
|
|
f3e50120ee | ||
|
|
850510b7e8 | ||
|
|
d1e9a1221f | ||
|
|
56ea28aa83 | ||
|
|
140ae1d049 | ||
|
|
6025323c4b | ||
|
|
261c83158b | ||
|
|
1a5f16c4e3 | ||
|
|
ab0bf5ca5f | ||
|
|
158b66e43b | ||
|
|
ec99b5e9b4 | ||
|
|
19fe41d738 | ||
|
|
5c147562b3 | ||
|
|
051c17f454 | ||
|
|
ab8868e6c4 | ||
|
|
b5b600db1f | ||
|
|
41f917ddf6 | ||
|
|
e5e02b553a | ||
|
|
729f107af2 | ||
|
|
8bfb187ef0 | ||
|
|
1c47c8ce2c | ||
|
|
acc910cbc0 | ||
|
|
1aedf21566 | ||
|
|
6cb538ce69 | ||
|
|
cafd9f9899 | ||
|
|
bb2ab8e08a | ||
|
|
3059e730e2 | ||
|
|
dc321580c3 | ||
|
|
6cfa77a335 | ||
|
|
ddf3e8251d | ||
|
|
0cfe9d60fb | ||
|
|
19d3ae119c | ||
|
|
d59e2796e7 | ||
|
|
12ff66fd3e | ||
|
|
35270e2761 | ||
|
|
47021fa55c | ||
|
|
89f5be2cc9 | ||
|
|
d3493a0af6 | ||
|
|
157343d152 | ||
|
|
5f36543d44 | ||
|
|
a004b7b450 | ||
|
|
56d8224fb9 | ||
|
|
dd6e7b7e7e | ||
|
|
6d5034e2a5 | ||
|
|
ed6882ba95 | ||
|
|
b3f8d77238 | ||
|
|
c287da92d9 | ||
|
|
aea79fec2e | ||
|
|
3239f555a1 | ||
|
|
da49e15589 | ||
|
|
cfebc770fd | ||
|
|
ffef14957f | ||
|
|
05666f70e0 | ||
|
|
c1dc575f53 | ||
|
|
bf861700fa | ||
|
|
627c2174a9 | ||
|
|
b27eccec88 | ||
|
|
8c0dfb2cc5 | ||
|
|
c766dc9f00 | ||
|
|
7236e85321 | ||
|
|
b7093b5de5 | ||
|
|
3650c400ca | ||
|
|
4933d11dfd | ||
|
|
08128279b4 | ||
|
|
181e149028 | ||
|
|
9bb9265a19 | ||
|
|
65f232161f | ||
|
|
df97b93a9a | ||
|
|
f58d05f1b3 | ||
|
|
06fa79e09d | ||
|
|
6da52adc74 | ||
|
|
0b4ec46f79 | ||
|
|
6e321afa72 | ||
|
|
a08ded4093 | ||
|
|
e76b44fcba | ||
|
|
1fa2dbcf85 | ||
|
|
74fd20cfa8 | ||
|
|
f90b5bab4b | ||
|
|
812cff0fd9 | ||
|
|
7127c28c35 | ||
|
|
4f186a6b49 | ||
|
|
fe76416059 | ||
|
|
ac2b7bd65d | ||
|
|
604b20855b | ||
|
|
e157ee7c53 | ||
|
|
91ccfe26b7 | ||
|
|
de88e1d442 | ||
|
|
baf5967dca | ||
|
|
df6a1b39e0 | ||
|
|
8fcaf721ad | ||
|
|
22d17708cd | ||
|
|
457408006b | ||
|
|
f74289fe49 | ||
|
|
86ece89cec | ||
|
|
22826afd21 | ||
|
|
73f2f29c2a | ||
|
|
a6eb2735aa | ||
|
|
c4d42dd9e3 | ||
|
|
e047309812 | ||
|
|
c11da04e13 | ||
|
|
4f68a76674 | ||
|
|
b2fdb11195 | ||
|
|
97082b30b9 | ||
|
|
43ac0174d1 | ||
|
|
cb02bb0de7 | ||
|
|
9c9eaca990 | ||
|
|
8336ea199d | ||
|
|
48848d1d43 | ||
|
|
1eea0b6862 | ||
|
|
2977c1f6db | ||
|
|
c70ee26c8a | ||
|
|
1b085236c7 | ||
|
|
78b371bfad | ||
|
|
fdff3be316 | ||
|
|
f2037bf5fe | ||
|
|
a28a8fe5f9 | ||
|
|
62f94cfb20 | ||
|
|
3e75972b02 | ||
|
|
d6d3b53846 | ||
|
|
87f843df1c | ||
|
|
5dbec377e5 | ||
|
|
a489c9ff45 | ||
|
|
4d989746fd | ||
|
|
ba48461417 | ||
|
|
55e2d5c0e3 | ||
|
|
8864f9799f | ||
|
|
e2ddffe24b | ||
|
|
12b7819791 | ||
|
|
bfa6832beb | ||
|
|
846564a17e | ||
|
|
46d7009837 | ||
|
|
0c04db3299 | ||
|
|
2d437e2016 | ||
|
|
37655a64c0 | ||
|
|
4921b17f11 | ||
|
|
dae0d6cc42 | ||
|
|
c392933e55 | ||
|
|
c915137ed7 | ||
|
|
945cff978b | ||
|
|
ae21b728bc | ||
|
|
8ffd9a5e2a | ||
|
|
b823f0f7db | ||
|
|
066df2c64f | ||
|
|
200a933647 | ||
|
|
4cd5cd4bae | ||
|
|
bfba1d148e | ||
|
|
c58801f6f7 | ||
|
|
616927d388 | ||
|
|
1cbebf322b | ||
|
|
a46c032979 | ||
|
|
f03f94c3dd | ||
|
|
1cee10842e | ||
|
|
55e9c74031 | ||
|
|
45a9495a51 | ||
|
|
c785704dd8 | ||
|
|
38bb915144 | ||
|
|
e2f99f52db | ||
|
|
c54feb9306 | ||
|
|
e11b09feac | ||
|
|
fe44bc989a | ||
|
|
93bf98fe72 | ||
|
|
5a2626b38b | ||
|
|
5d57138ea8 | ||
|
|
20519afe17 | ||
|
|
f23f7df147 | ||
|
|
b89ab5a192 | ||
|
|
35c59dc53b | ||
|
|
870cd6407e | ||
|
|
f62350fea0 | ||
|
|
39779b9dd5 | ||
|
|
9716e64c93 | ||
|
|
c86c5d8235 | ||
|
|
86160a103d | ||
|
|
2bb7818116 | ||
|
|
39d666df6b | ||
|
|
a7e26d553f | ||
|
|
95e82cf254 | ||
|
|
07c8334248 | ||
|
|
5b395b96cd | ||
|
|
b1b4fd3f64 | ||
|
|
6e76e29768 | ||
|
|
2c1a01bb6a | ||
|
|
1cbec8220d | ||
|
|
1456c4f749 | ||
|
|
881944ab98 | ||
|
|
cb2016a5dc | ||
|
|
9040a99853 | ||
|
|
eb8cf1b6da | ||
|
|
c633a9ad8d | ||
|
|
c3f5d249a0 | ||
|
|
ce103c6b59 | ||
|
|
38f5c41272 | ||
|
|
ca13185d84 | ||
|
|
f1c6beacf2 | ||
|
|
923eda1307 | ||
|
|
997342a31c |
No files matched your search
@@ -0,0 +1 @@
|
||||
release_notes: false
|
||||
@@ -6,7 +6,9 @@ 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 LANGUAGES = (
|
||||
process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
|
||||
).split(",");
|
||||
const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json";
|
||||
|
||||
// POEditor API endpoint
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
- name: Install client dependencies
|
||||
working-directory: client
|
||||
run: npm ci
|
||||
run: npm install
|
||||
|
||||
- name: Check client formatting
|
||||
working-directory: client
|
||||
@@ -34,29 +34,8 @@ jobs:
|
||||
|
||||
- name: Install server dependencies
|
||||
working-directory: server
|
||||
run: npm ci
|
||||
run: npm install
|
||||
|
||||
- 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."
|
||||
@@ -79,18 +79,6 @@ jobs:
|
||||
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:
|
||||
|
||||
@@ -74,18 +74,6 @@ jobs:
|
||||
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:
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
languages:
|
||||
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
|
||||
required: false
|
||||
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr"
|
||||
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
|
||||
format:
|
||||
description: "Export format (key_value_json or json)"
|
||||
required: false
|
||||
|
||||
@@ -71,17 +71,6 @@ jobs:
|
||||
- name: Push MongoDB Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-demo
|
||||
|
||||
- name: Build Redis Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:redis-demo \
|
||||
-f ./docker/prod/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-demo
|
||||
|
||||
deploy-to-demo:
|
||||
needs: docker-build-and-push-server
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -71,17 +71,6 @@ jobs:
|
||||
- name: Push MongoDB Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-staging
|
||||
|
||||
- name: Build Redis Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:redis-staging \
|
||||
-f ./docker/staging/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-staging
|
||||
|
||||
deploy-to-staging:
|
||||
needs: docker-build-and-push-server
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.vscode
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://www.bestpractices.dev/projects/9901)
|
||||
[](https://deepwiki.com/bluewave-labs/checkmate)
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Una aplicación de código abierto para monitoreo de infraestructura y tiempo de actividad</strong></p>
|
||||
|
||||
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
|
||||
|
||||
Este repositorio contiene tanto el frontend como el backend de Checkmate, una herramienta de monitoreo de código abierto y autoalojada para rastrear hardware de servidores, tiempo de actividad, tiempos de respuesta e incidentes en tiempo real con visualizaciones atractivas. Checkmate revisa regularmente si un servidor o sitio web es accesible y funciona de manera óptima, proporcionando alertas e informes en tiempo real sobre la disponibilidad, el tiempo de inactividad y el tiempo de respuesta de los servicios monitoreados.
|
||||
|
||||
Checkmate también tiene un agente llamado [Capture](https://github.com/bluewave-labs/capture), para recuperar datos de servidores remotos. Aunque Capture no es obligatorio para ejecutar Checkmate, proporciona información adicional sobre el estado de la CPU, RAM, disco y temperatura de tus servidores.
|
||||
|
||||
Checkmate ha sido probado con más de 1000 monitores activos sin problemas ni cuellos de botella de rendimiento.
|
||||
|
||||
**Si deseas patrocinar una función, [visita este enlace](https://checkmate.so/sponsored-features).**
|
||||
|
||||
## 📚 Tabla de contenidos
|
||||
|
||||
- [📦 Demo](#demo)
|
||||
- [🔗 Guía del usuario](#guía-del-usuario)
|
||||
- [🛠️ Instalación](#instalación)
|
||||
- [🏁 Traducciones](#traducciones)
|
||||
- [🚀 Rendimiento](#rendimiento)
|
||||
- [💚 Preguntas e ideas](#preguntas-e-ideas)
|
||||
- [🧩 Características](#características)
|
||||
- [🏗️ Capturas de pantalla](#capturas-de-pantalla)
|
||||
- [🏗️ Tecnologías](#tecnologías)
|
||||
- [🔗 Enlaces útiles](#enlaces-útiles)
|
||||
- [🤝 Contribuciones](#contribuciones)
|
||||
- [💰 Patrocinadores](#patrocinadores)
|
||||
|
||||
...
|
||||
|
||||
**[Texto truncado para mantener la longitud del mensaje manejable]**
|
||||
|
||||
## Demo
|
||||
|
||||
Puedes ver la última versión de [Checkmate](https://checkmate-demo.bluewavelabs.ca/) en acción. El usuario es uptimedemo@demo.com y la contraseña es Demouser1! (ten en cuenta que actualizamos el servidor de demostración de vez en cuando, así que si no funciona para ti, por favor contáctanos en el canal de Discusiones).
|
||||
|
||||
## Guía del usuario
|
||||
|
||||
Las instrucciones de uso se pueden encontrar [aquí](https://docs.checkmate.so/checkmate-2.1). Todavía está en desarrollo y parte de la información puede estar desactualizada ya que continuamente añadimos funciones cada semana. ¡Ten por seguro que estamos haciendo lo mejor posible! :)
|
||||
|
||||
## Instalación
|
||||
|
||||
Consulta las instrucciones de instalación en el [portal de documentación de Checkmate](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
|
||||
|
||||
Alternativamente, también puedes usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) o [Pikapods](https://www.pikapods.com/) para desplegar rápidamente una instancia de Checkmate. Si deseas monitorear tu infraestructura de servidores, necesitarás el agente [Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture también contiene las instrucciones de instalación.
|
||||
|
||||
## Traducciones
|
||||
|
||||
Si deseas usar Checkmate en tu idioma, por favor [ve a esta página](https://poeditor.com/join/project/lRUoGZFCsJ) y regístrate para el idioma al que te gustaría traducir Checkmate.
|
||||
|
||||
## Rendimiento
|
||||
|
||||
Gracias a extensas optimizaciones, Checkmate opera con un uso de memoria excepcionalmente bajo, requiriendo recursos mínimos de memoria y CPU. Aquí está el uso de memoria de una instancia de Node.js ejecutándose en un servidor que monitorea 323 servidores cada minuto:
|
||||
|
||||

|
||||
|
||||
También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servidor (398Mb y 15Mb) para la misma cantidad de servidores:
|
||||
|
||||

|
||||
|
||||
## Preguntas e Ideas
|
||||
|
||||
Si tienes alguna pregunta, sugerencia o comentario, tienes varias opciones:
|
||||
|
||||
- [Canal de Discord](https://discord.gg/NAb6H3UTjK)
|
||||
- [Discusiones en GitHub](https://github.com/bluewave-labs/bluewave-uptime/discussions)
|
||||
- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/)
|
||||
|
||||
¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti!
|
||||
|
||||
## Características
|
||||
|
||||
- Completamente de código abierto, desplegable en tus propios servidores o dispositivos personales (por ejemplo, Raspberry Pi 4 o 5)
|
||||
- Monitoreo de sitios web
|
||||
- Monitoreo de velocidad de carga
|
||||
- Monitoreo de infraestructura (memoria, uso de disco, rendimiento de CPU, etc) - requiere el agente [Capture](https://github.com/bluewave-labs/capture)
|
||||
- Monitoreo de Docker
|
||||
- Monitoreo con ping
|
||||
- Monitoreo de certificados SSL
|
||||
- Monitoreo de puertos
|
||||
- Incidentes en una sola vista
|
||||
- Páginas de estado
|
||||
- Notificaciones por correo, Webhooks, Discord, Telegram, Slack
|
||||
- Mantenimiento programado
|
||||
- Monitoreo de consultas JSON
|
||||
- Soporte para múltiples idiomas
|
||||
|
||||
**Hoja de ruta a corto plazo:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
|
||||
|
||||
- Mejores notificaciones
|
||||
- Monitoreo de red
|
||||
- ...y algunas funciones más
|
||||
|
||||
## Capturas de pantalla
|
||||
|
||||
<p>
|
||||
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
|
||||
</p>
|
||||
<p>
|
||||
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
|
||||
</p>
|
||||
<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>
|
||||
|
||||
## Tecnologías
|
||||
|
||||
- [ReactJs](https://react.dev/)
|
||||
- [MUI (framework de React)](https://mui.com/)
|
||||
- [Node.js](https://nodejs.org/en)
|
||||
- [MongoDB](https://mongodb.com)
|
||||
- [Recharts](https://recharts.org)
|
||||
- ¡Y muchos otros componentes de código abierto!
|
||||
|
||||
## Enlaces útiles
|
||||
|
||||
- Si deseas apoyarnos, por favor considera darle una ⭐ y haz clic en "watch".
|
||||
- ¿Tienes una pregunta o sugerencia para la hoja de ruta? Revisa nuestro [canal de Discord](https://discord.gg/NAb6H3UTjK) o el foro de [Discusiones](https://github.com/bluewave-labs/checkmate/discussions).
|
||||
- ¿Quieres saber cuándo hay una nueva versión? Usa [Newreleases](https://newreleases.io/), un servicio gratuito para seguir lanzamientos.
|
||||
- Mira un video de instalación y uso de Checkmate [aquí](https://www.youtube.com/watch?v=GfFOc0xHIwY)
|
||||
|
||||
## Contribuciones
|
||||
|
||||
Somos [Alex](http://github.com/ajhollid) (líder de equipo), [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) y [Mert](https://github.com/mertssmnoglu), ayudando a personas y empresas a monitorear su infraestructura y servidores.
|
||||
|
||||
Nos enorgullecemos de construir conexiones fuertes con contribuyentes de todos los niveles. A pesar de ser un proyecto joven, Checkmate ya ha ganado más de 7000 estrellas y ha atraído a más de 90 contribuyentes de todo el mundo.
|
||||
|
||||
Nuestro repositorio ha sido marcado con estrella por empleados de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes y NEC**, ¡así que no te detengas — participa, contribuye y aprende con nosotros!
|
||||
|
||||
Cómo contribuir:
|
||||
|
||||
0. Dale una estrella al repositorio :)
|
||||
1. Revisa la [guía para contribuidores](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Se anima a los nuevos a revisar las etiquetas `good-first-issue`.
|
||||
2. Consulta la [estructura del proyecto](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) y la [visión general](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
|
||||
3. Lee una estructura detallada de [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) si deseas profundizar en la arquitectura.
|
||||
4. Abre un issue si crees que has encontrado un error.
|
||||
5. Revisa los issues con la etiqueta `good-first-issue` si eres nuevo.
|
||||
6. Haz un pull request para añadir nuevas funciones, mejoras o correcciones.
|
||||
|
||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
</a>
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
|
||||
|
||||
## Patrocinadores
|
||||
|
||||
Gracias a [Gitbook](https://gitbook.io/) por darnos una cuenta gratuita para su plataforma de documentación, y a [Poeditor](https://poeditor.com/) por proporcionarnos una cuenta gratuita para servicios de traducción. Si deseas patrocinar Checkmate, por favor envía un correo a hello@bluewavelabs.ca
|
||||
|
||||
Si deseas patrocinar una función, [visita esta página](https://checkmate.so/sponsored-features).
|
||||
|
||||
También puedes revisar otros proyectos de BlueWave orientados a desarrolladores y contribuidores:
|
||||
|
||||
- [VerifyWise](https://github.com/bluewave-labs/verifywise), la primera plataforma de gobernanza de IA de código abierto.
|
||||
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), una aplicación de intercambio de archivos seguro, también conocida como dataroom.
|
||||
- [Guidefox](https://github.com/bluewave-labs/guidefox), una app que ayuda a nuevos usuarios a aprender a usar tu producto mediante pistas, tours, popups y banners.
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
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.
|
||||
|
||||
Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status.
|
||||
Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. Capture can run on Linux, Windows, Mac, Raspberry Pi, or any device that can run Go.
|
||||
|
||||
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
|
||||
|
||||
@@ -26,41 +26,46 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
||||
|
||||
## 📚 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)
|
||||
- [📦 Demo](#demo)
|
||||
- [🔗 User's guide](#users-guide)
|
||||
- [🛠️ Installation](#installation)
|
||||
- [🏁 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)
|
||||
- [🤝 Contributing](#contributing)
|
||||
- [💰 Our sponsors](#our-sponsors)
|
||||
|
||||
|
||||
## 📦 Demo
|
||||
## Demo
|
||||
|
||||
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
||||
|
||||
## 🔗 User's guide
|
||||
## User's guide
|
||||
|
||||
Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
|
||||
|
||||
## 🛠️ Installation
|
||||
## 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/), [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.
|
||||
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) 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.
|
||||
|
||||
### Using a Custom CA
|
||||
|
||||
If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our [Custom CA Trust Guide](./docs/custom-ca-trust.md) for Docker configuration options.
|
||||
|
||||
For more documentation, see the [docs directory](./docs/).
|
||||
|
||||
|
||||
## 🏁 Translations
|
||||
## Translations
|
||||
|
||||
If you would like to use Checkmate in your language, please [go to this page](https://poeditor.com/join/project/lRUoGZFCsJ) and register for the language you would like to translate Checkmate to.
|
||||
|
||||
## 🚀 Performance
|
||||
## Performance
|
||||
|
||||
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
|
||||
|
||||
@@ -70,34 +75,44 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
||||
|
||||

|
||||
|
||||
## 💚 Questions & Ideas
|
||||
## Questions & Ideas
|
||||
|
||||
If you have any questions, suggestions or comments, please use our [Discord channel](https://discord.gg/NAb6H3UTjK). We've also launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you!
|
||||
If you have any questions, suggestions or comments, you have several options:
|
||||
|
||||
## 🧩 Features
|
||||
- [Discord channel](https://discord.gg/NAb6H3UTjK)
|
||||
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions)
|
||||
- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/)
|
||||
|
||||
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
|
||||
## Features
|
||||
|
||||
- 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)
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
|
||||
- Docker monitoring
|
||||
- Ping monitoring
|
||||
- SSL monitoring
|
||||
- Port monitoring
|
||||
- Game server monitoring (3.0)
|
||||
- Incidents at a glance
|
||||
- Status pages
|
||||
- E-mail, Webhooks, Discord, Telegram, Slack notifications
|
||||
- E-mail, Webhooks, Discord and Slack notifications
|
||||
- Scheduled maintenance
|
||||
- JSON query monitoring
|
||||
- Support for multiple languages
|
||||
- Multi-language support for English, German, Japanese, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, Chinese (Traditional, Taiwan)
|
||||
|
||||
**Short term roadmap:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
|
||||
**Short term roadmap:**
|
||||
|
||||
- Plugins that will help Checkmate get any information from a remote service (e.g database, etc)
|
||||
- Better notifications
|
||||
- Network monitoring
|
||||
- ..and a few more features
|
||||
|
||||
## 🏗️ Screenshots
|
||||
If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features).
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p>
|
||||
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
|
||||
@@ -114,7 +129,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
|
||||
|
||||
|
||||
## 🏗️ Tech stack
|
||||
## Tech stack
|
||||
|
||||
- [ReactJs](https://react.dev/)
|
||||
- [MUI (React framework)](https://mui.com/)
|
||||
@@ -130,11 +145,11 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
- Need a ping when there's a new release? Use [Newreleases](https://newreleases.io/), a free service to track releases.
|
||||
- Watch a Checkmate [installation and usage video](https://www.youtube.com/watch?v=GfFOc0xHIwY)
|
||||
|
||||
## 🤝 Contributing
|
||||
## 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), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) 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 6000+ stars and attracted 80+ 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 7000+ stars and attracted 90+ 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!
|
||||
|
||||
@@ -154,7 +169,7 @@ Here's how you can contribute:
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
|
||||
|
||||
## 💰 Our sponsors
|
||||
## 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
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@ metadata:
|
||||
name: checkmate-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- range $key, $value := := $secrets }}
|
||||
{{- range $key, $value := $secrets }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
@@ -20,11 +20,3 @@ spec:
|
||||
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
|
||||
@@ -5,6 +5,11 @@ metadata:
|
||||
name: server-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}"
|
||||
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
||||
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
|
||||
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.server.ingress.host }}
|
||||
|
||||
Generated
+20
-627
@@ -21,7 +21,7 @@
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"mui-color-input": "^6.0.0",
|
||||
"react": "18.3.1",
|
||||
@@ -45,10 +45,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "6.3.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-arm64-musl": "4.41.0"
|
||||
"vite": "6.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -283,9 +280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
||||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
||||
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -491,262 +488,6 @@
|
||||
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||
@@ -763,134 +504,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
@@ -2063,123 +1676,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
|
||||
"integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz",
|
||||
"integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz",
|
||||
"integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz",
|
||||
"integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz",
|
||||
"integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz",
|
||||
"integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz",
|
||||
"integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz",
|
||||
"integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
|
||||
@@ -2193,71 +1689,6 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz",
|
||||
"integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz",
|
||||
"integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
|
||||
@@ -2284,45 +1715,6 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz",
|
||||
"integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz",
|
||||
"integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz",
|
||||
"integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
@@ -3030,13 +2422,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -4342,14 +3734,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4690,9 +4083,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "24.2.3",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
|
||||
"integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==",
|
||||
"version": "25.4.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz",
|
||||
"integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -4709,7 +4102,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10"
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
@@ -6996,9 +6389,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
+2
-5
@@ -26,7 +26,7 @@
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"mui-color-input": "^6.0.0",
|
||||
"react": "18.3.1",
|
||||
@@ -58,9 +58,6 @@
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "6.3.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-arm64-musl": "4.41.0"
|
||||
"vite": "6.3.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import LeftArrow from "../../assets/icons/left-arrow.svg?react";
|
||||
import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react";
|
||||
import LeftArrowLong from "../../assets/icons/left-arrow-long.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<LeftArrowDouble
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (type === "long") {
|
||||
return (
|
||||
<LeftArrowLong
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LeftArrow
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ArrowLeft.propTypes = {
|
||||
color: PropTypes.string,
|
||||
type: PropTypes.oneOf(["double", "long", "default"]),
|
||||
};
|
||||
export default ArrowLeft;
|
||||
@@ -0,0 +1,28 @@
|
||||
import RightArrow from "../../assets/icons/right-arrow.svg?react";
|
||||
import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ArrowRight = ({ type, color = "#667085", ...props }) => {
|
||||
if (type === "double") {
|
||||
return (
|
||||
<RightArrowDouble
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RightArrow
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ArrowRight.propTypes = {
|
||||
type: PropTypes.oneOf(["double", "default"]),
|
||||
color: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ArrowRight;
|
||||
@@ -15,7 +15,7 @@ import { useTheme } from "@emotion/react";
|
||||
* <Avatar src="assets/img" first="Alex" last="Holliday" small />
|
||||
*/
|
||||
|
||||
const Avatar = ({ src, small, sx }) => {
|
||||
const Avatar = ({ src, small, sx, onClick = () => {} }) => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -31,6 +31,7 @@ const Avatar = ({ src, small, sx }) => {
|
||||
|
||||
return (
|
||||
<MuiAvatar
|
||||
onClick={onClick}
|
||||
alt={`${user?.firstName} ${user?.lastName}`}
|
||||
/* TODO What is the /static/images/avatar/2.jpg ?*/
|
||||
src={src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"}
|
||||
@@ -66,6 +67,7 @@ Avatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
small: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
@@ -2,8 +2,7 @@ import PropTypes from "prop-types";
|
||||
import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
|
||||
|
||||
import ArrowRight from "../ArrowRight";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
@@ -15,19 +17,26 @@ const MAXIMUM_VALUE = 100;
|
||||
* @param {number} [props.progress=0] - Progress percentage (0-100)
|
||||
* @param {number} [props.radius=60] - Radius of the gauge circle
|
||||
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
|
||||
* @param {number} [props.threshold=50] - Threshold for color change
|
||||
* @param {number} [props.precision=1] - Precision of the progress percentage
|
||||
* @param {string} [props.unit="%"] - Unit of progress
|
||||
*
|
||||
* @example
|
||||
* <CustomGauge
|
||||
* progress={75}
|
||||
* radius={50}
|
||||
* strokeWidth={10}
|
||||
* threshold={50}
|
||||
* />
|
||||
*
|
||||
* @returns {React.ReactElement} Rendered CustomGauge component
|
||||
*/
|
||||
const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
|
||||
const CustomGauge = ({
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
// Calculate the length of the stroke for the circle
|
||||
const { circumference, totalSize, strokeLength } = useMemo(
|
||||
@@ -52,10 +61,28 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
|
||||
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
|
||||
|
||||
const fillColor =
|
||||
progressWithinRange > threshold
|
||||
? theme.palette.error.lowContrast // CAIO_REVIEW
|
||||
: theme.palette.accent.main; // CAIO_REVIEW
|
||||
let fillColor;
|
||||
if (progressWithinRange < 50) {
|
||||
fillColor = theme.palette.success.main;
|
||||
} else if (progressWithinRange < 80) {
|
||||
fillColor = theme.palette.warning.lowContrast;
|
||||
} else {
|
||||
fillColor = theme.palette.error.lowContrast;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack
|
||||
className="radial-chart"
|
||||
width={radius}
|
||||
height={radius}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<CircularProgress color="accent" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -71,7 +98,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
>
|
||||
<circle
|
||||
className="radial-chart-base"
|
||||
stroke={theme.palette.secondary.light} // CAIO_REVIEW
|
||||
stroke={theme.palette.secondary.light}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
cx={totalSize / 2} // Center the circle
|
||||
@@ -103,7 +130,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
fill: theme.typography.h2.color,
|
||||
}}
|
||||
>
|
||||
{`${progressWithinRange.toFixed(1)}%`}
|
||||
{`${progressWithinRange.toFixed(precision)}${unit}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@@ -112,8 +139,10 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
export default CustomGauge;
|
||||
|
||||
CustomGauge.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
progress: PropTypes.number,
|
||||
radius: PropTypes.number,
|
||||
strokeWidth: PropTypes.number,
|
||||
threshold: PropTypes.number,
|
||||
precision: PropTypes.number,
|
||||
unit: PropTypes.string,
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { formatDateWithTz } from "../../../Utils/timeUtils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, forwardRef } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
@@ -17,26 +17,32 @@ import PropTypes from "prop-types";
|
||||
* @returns {JSX.Element} The Bar component.
|
||||
*/
|
||||
|
||||
const Bar = ({ width, height, backgroundColor, borderRadius, children }) => {
|
||||
const theme = useTheme();
|
||||
const Bar = forwardRef(
|
||||
({ width, height, backgroundColor, borderRadius, children, ...otherProps }, ref) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
width={width}
|
||||
height={height}
|
||||
backgroundColor={backgroundColor}
|
||||
sx={{
|
||||
borderRadius: borderRadius || theme.spacing(1.5),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
position="relative"
|
||||
width={width}
|
||||
height={height}
|
||||
backgroundColor={backgroundColor}
|
||||
sx={{
|
||||
borderRadius: borderRadius || theme.spacing(1.5),
|
||||
}}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Bar.displayName = "Bar";
|
||||
|
||||
Bar.propTypes = {
|
||||
width: PropTypes.string.isRequired,
|
||||
width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
height: PropTypes.string.isRequired,
|
||||
backgroundColor: PropTypes.string.isRequired,
|
||||
borderRadius: PropTypes.string,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const tooltipDateFormatLookup = (dateRange) => {
|
||||
const dateFormatLookup = {
|
||||
recent: "ddd. MMMM D, YYYY, hh:mm A",
|
||||
day: "ddd. MMMM D, YYYY, hh:mm A",
|
||||
week: "ddd. MMMM D, YYYY, hh:mm A",
|
||||
month: "ddd. MMMM D, YYYY",
|
||||
|
||||
@@ -88,7 +88,51 @@ PercentTick.propTypes = {
|
||||
*/
|
||||
const getFormattedPercentage = (value) => {
|
||||
if (typeof value !== "number") return value;
|
||||
return `${(value * 100).toFixed(2)}.%`;
|
||||
return `${(value * 100).toFixed(2)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom tick component for rendering network bytes per second.
|
||||
*
|
||||
* @param {Object} props - The properties object.
|
||||
* @param {number} props.x - The x-coordinate for the tick.
|
||||
* @param {number} props.y - The y-coordinate for the tick.
|
||||
* @param {Object} props.payload - The payload object containing tick data.
|
||||
* @param {number} props.index - The index of the tick.
|
||||
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
|
||||
*/
|
||||
export const NetworkTick = ({ x, y, payload, index, formatter }) => {
|
||||
const theme = useTheme();
|
||||
if (index === 0) return null;
|
||||
|
||||
if (formatter === undefined) {
|
||||
formatter = (value, space = false) => {
|
||||
if (typeof value !== "number") return value;
|
||||
// need to add space between value and unit
|
||||
return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`;
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
x={x - 20}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
fill={theme.palette.primary.contrastTextTertiary}
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatter(payload?.value, true)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
NetworkTick.propTypes = {
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
payload: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
formatter: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -112,6 +156,7 @@ export const InfrastructureTooltip = ({
|
||||
yLabel,
|
||||
dotColor,
|
||||
dateRange,
|
||||
formatter = getFormattedPercentage,
|
||||
}) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const theme = useTheme();
|
||||
@@ -166,8 +211,8 @@ export const InfrastructureTooltip = ({
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{yIdx >= 0
|
||||
? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}`
|
||||
: `${yLabel} ${getFormattedPercentage(payload[0].payload[yKey])}`}
|
||||
? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}`
|
||||
: `${yLabel} ${formatter(payload[0].payload[yKey])}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -51,9 +51,10 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
|
||||
sx={{
|
||||
color:
|
||||
variant === "info"
|
||||
? theme.palette.primary.contrastTextTertiary
|
||||
? theme.palette.primary.contrastTextSecondary
|
||||
: colors[variant],
|
||||
opacity: 0.8,
|
||||
opacity: 0.9,
|
||||
fontWeight: 450,
|
||||
}}
|
||||
>
|
||||
{noHighlightText && <Typography component="span">{noHighlightText}</Typography>}{" "}
|
||||
@@ -64,7 +65,7 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
|
||||
};
|
||||
|
||||
Check.propTypes = {
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
noHighlightText: PropTypes.string,
|
||||
variant: PropTypes.oneOf(["info", "error", "success"]),
|
||||
outlined: PropTypes.bool,
|
||||
|
||||
@@ -4,15 +4,14 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing(20),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.spacing(2),
|
||||
"& > *": {
|
||||
paddingTop: theme.spacing(12),
|
||||
paddingBottom: theme.spacing(18),
|
||||
paddingTop: theme.spacing(15),
|
||||
paddingBottom: theme.spacing(15),
|
||||
},
|
||||
"& > div:first-of-type": {
|
||||
flex: 0.7,
|
||||
@@ -21,11 +20,16 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
|
||||
borderRightColor: theme.palette.primary.lowContrast,
|
||||
paddingRight: theme.spacing(15),
|
||||
paddingLeft: theme.spacing(15),
|
||||
backgroundColor: theme.palette.tertiary.background,
|
||||
"& :is(h1, h2):first-of-type": {
|
||||
fontWeight: 600,
|
||||
marginBottom: theme.spacing(4),
|
||||
},
|
||||
},
|
||||
"& > div:last-of-type": {
|
||||
flex: 1,
|
||||
paddingRight: theme.spacing(20),
|
||||
paddingLeft: theme.spacing(18),
|
||||
paddingLeft: theme.spacing(20),
|
||||
},
|
||||
"& h1, & h2": {
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useId } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Modal, Stack, Typography } from "@mui/material";
|
||||
|
||||
const GenericDialog = ({ title, description, open, onClose, theme, children }) => {
|
||||
const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => {
|
||||
const titleId = useId();
|
||||
const descriptionId = useId();
|
||||
const ariaDescribedBy = description?.length > 0 ? descriptionId : "";
|
||||
@@ -16,6 +16,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
width={width}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
@@ -39,6 +40,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
|
||||
fontSize={16}
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontWeight={600}
|
||||
marginBottom={theme.spacing(4)}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
@@ -46,6 +48,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
|
||||
<Typography
|
||||
id={descriptionId}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
marginBottom={theme.spacing(4)}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
@@ -64,6 +67,7 @@ GenericDialog.propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
|
||||
.isRequired,
|
||||
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
|
||||
};
|
||||
|
||||
export { GenericDialog };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Button, Stack } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const FallbackActionButtons = ({ link, type }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ fontWeight: 700 }}
|
||||
onClick={() => navigate(link)}
|
||||
>
|
||||
{t(`${type}.fallback.actionButton`)}
|
||||
</Button>
|
||||
{type === "uptimeMonitor" && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate("/uptime/bulk-import")}
|
||||
>
|
||||
{t("bulkImport.fallbackPage")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
FallbackActionButtons.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
link: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FallbackActionButtons;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import OutputAnimation from "../../assets/Animations/output.gif";
|
||||
import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif";
|
||||
import { useSelector } from "react-redux";
|
||||
const FallbackBackground = () => {
|
||||
const theme = useTheme();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
component="img"
|
||||
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
||||
background="transparent"
|
||||
alt="Loading animation"
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
border: "none",
|
||||
borderRadius: theme.spacing(8),
|
||||
width: "100%",
|
||||
transform: "scale(0.6667)",
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FallbackBackground;
|
||||
@@ -0,0 +1,35 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Check from "../Check/Check";
|
||||
const FallbackCheckList = ({ checks, type }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: theme.spacing(2),
|
||||
alignItems: "flex-start",
|
||||
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
|
||||
}}
|
||||
>
|
||||
{checks?.map((check, index) => (
|
||||
<Check
|
||||
text={check}
|
||||
key={`${type.trim().split(" ")[0]}-${index}`}
|
||||
outlined={true}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
FallbackCheckList.propTypes = {
|
||||
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FallbackCheckList;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const FallbackContainer = ({ children, type }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
border={1}
|
||||
borderColor={theme.palette.tertiary.border}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.tertiary.background}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderStyle: "dashed",
|
||||
height: "fit-content",
|
||||
minHeight: "60vh",
|
||||
width: {
|
||||
sm: "90%",
|
||||
md: "70%",
|
||||
lg: "50%",
|
||||
xl: "40%",
|
||||
},
|
||||
padding: `${theme.spacing(20)} ${theme.spacing(10)}`,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
className={`fallback__${type?.trim().split(" ")[0]}`}
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
FallbackContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FallbackContainer;
|
||||
@@ -0,0 +1,69 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Link from "@mui/material/Link";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Alert from "../Alert";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const renderWarningMessage = (t) => {
|
||||
return (
|
||||
<>
|
||||
{t("pageSpeedWarning")}{" "}
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/settings"
|
||||
sx={{
|
||||
textDecoration: "underline",
|
||||
color: "inherit",
|
||||
fontWeight: "inherit",
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("pageSpeedLearnMoreLink")}
|
||||
</Link>{" "}
|
||||
{t("pageSpeedAddApiKey")}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FallbackPageSpeedWarning = ({ settingsData }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<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(t)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
FallbackPageSpeedWarning.propTypes = {
|
||||
settingsData: PropTypes.shape({
|
||||
pagespeedKeySet: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default FallbackPageSpeedWarning;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const FallbackTitle = ({ title }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Typography
|
||||
alignSelf="center"
|
||||
component="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
FallbackTitle.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
export default FallbackTitle;
|
||||
@@ -38,7 +38,3 @@
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.fallback__status > .MuiStack-root {
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -1,72 +1,26 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Stack, Typography, Link } from "@mui/material";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
|
||||
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import Check from "../Check/Check";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import Alert from "../Alert";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import "./index.css";
|
||||
import { useFetchSettings } from "../../Hooks/settingsHooks";
|
||||
import { useState } from "react";
|
||||
import FallbackTitle from "./FallbackTitle";
|
||||
import FallbackCheckList from "./FallbackCheckList";
|
||||
import FallbackActionButtons from "./FallBackActionButtons";
|
||||
import FallbackBackground from "./FallbackBackground";
|
||||
import FallbackContainer from "./FallbackContainer";
|
||||
/**
|
||||
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.title - The title to be displayed in the fallback UI.
|
||||
* @param {string} props.type - The type of the fallback (e.g., "pageSpeed", "notifications").
|
||||
* @param {Array<string>} props.checks - An array of strings representing the checks to display.
|
||||
* @param {string} [props.link="/"] - The link to navigate to.
|
||||
* @param {boolean} [props.vowelStart=false] - Whether the title starts with a vowel.
|
||||
* @param {boolean} [props.showPageSpeedWarning=false] - Whether to show the PageSpeed API warning.
|
||||
* @returns {JSX.Element} The rendered fallback UI.
|
||||
*/
|
||||
|
||||
const Fallback = ({
|
||||
title,
|
||||
checks,
|
||||
link = "/",
|
||||
isAdmin,
|
||||
vowelStart = false,
|
||||
showPageSpeedWarning = false,
|
||||
}) => {
|
||||
const Fallback = ({ title, checks, link = "/", isAdmin, type, children }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const { t } = useTranslation();
|
||||
const [settingsData, setSettingsData] = useState(undefined);
|
||||
|
||||
const [isLoading, error] = useFetchSettings({
|
||||
setSettingsData,
|
||||
setIsApiKeySet: () => {},
|
||||
setIsEmailPasswordSet: () => {},
|
||||
});
|
||||
// Custom warning message with clickable link
|
||||
const renderWarningMessage = () => {
|
||||
return (
|
||||
<>
|
||||
{t("pageSpeedWarning")}{" "}
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/settings"
|
||||
sx={{
|
||||
textDecoration: "underline",
|
||||
color: "inherit",
|
||||
fontWeight: "inherit",
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("pageSpeedLearnMoreLink")}
|
||||
</Link>{" "}
|
||||
{t("pageSpeedAddApiKey")}
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
@@ -78,138 +32,44 @@ const Fallback = ({
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<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),
|
||||
}}
|
||||
>
|
||||
<FallbackContainer type={type}>
|
||||
<FallbackBackground />
|
||||
<Stack
|
||||
className={`fallback__${title?.trim().split(" ")[0]}`}
|
||||
gap={theme.spacing(5)}
|
||||
zIndex={1}
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
{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={theme.spacing(180)}
|
||||
zIndex={1}
|
||||
>
|
||||
<Typography
|
||||
alignSelf="center"
|
||||
component="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{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", mb: "2px" }}
|
||||
onClick={() => navigate(link)}
|
||||
>
|
||||
Let's create your first {title}
|
||||
</Button>
|
||||
{/* Bulk create of uptime monitors */}
|
||||
{title === "uptime monitor" && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate("/uptime/bulk-import")}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
<FallbackTitle title={title} />
|
||||
<FallbackCheckList
|
||||
checks={checks}
|
||||
title={title}
|
||||
type={type}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
{isAdmin && (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
alignItems="center"
|
||||
>
|
||||
<FallbackActionButtons
|
||||
title={title}
|
||||
link={link}
|
||||
type={type}
|
||||
/>
|
||||
{children}
|
||||
</Stack>
|
||||
)}
|
||||
</FallbackContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Fallback.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
link: PropTypes.string,
|
||||
isAdmin: PropTypes.bool,
|
||||
vowelStart: PropTypes.bool,
|
||||
showPageSpeedWarning: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Fallback;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
|
||||
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
|
||||
import OutputAnimation from "../../assets/Animations/output.gif";
|
||||
import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
@@ -37,11 +37,18 @@ const GenericFallback = ({ children }) => {
|
||||
marginTop: "100px",
|
||||
}}
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Skeleton style={{ zIndex: 1 }} />
|
||||
) : (
|
||||
<SkeletonDark style={{ zIndex: 1 }} />
|
||||
)}
|
||||
<Box
|
||||
component="img"
|
||||
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
||||
Background="transparent"
|
||||
alt="Loading animation"
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
border: "none",
|
||||
borderRadius: theme.spacing(8),
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const DEFAULT_GAP = 6;
|
||||
const FieldWrapper = ({
|
||||
label,
|
||||
children,
|
||||
gap,
|
||||
labelMb,
|
||||
labelFontWeight = 500,
|
||||
labelVariant = "h3",
|
||||
labelSx = {},
|
||||
sx = {},
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
gap={gap ?? theme.spacing(DEFAULT_GAP)}
|
||||
sx={sx}
|
||||
>
|
||||
{label && (
|
||||
<Typography
|
||||
component={labelVariant}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={labelFontWeight}
|
||||
sx={{
|
||||
...(labelMb !== undefined && { mb: theme.spacing(labelMb) }),
|
||||
...labelSx,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
FieldWrapper.propTypes = {
|
||||
label: PropTypes.node,
|
||||
children: PropTypes.node.isRequired,
|
||||
gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
|
||||
labelMb: PropTypes.number,
|
||||
labelFontWeight: PropTypes.number,
|
||||
labelVariant: PropTypes.string,
|
||||
labelSx: PropTypes.object,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
export default FieldWrapper;
|
||||
@@ -24,7 +24,17 @@ import "./index.css";
|
||||
* @returns {JSX.Element} - The rendered Radio component.
|
||||
*/
|
||||
|
||||
const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
|
||||
const Radio = ({
|
||||
name,
|
||||
checked,
|
||||
value,
|
||||
id,
|
||||
size,
|
||||
title,
|
||||
desc,
|
||||
onChange,
|
||||
labelSpacing,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@@ -53,7 +63,14 @@ const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
|
||||
onChange={onChange}
|
||||
label={
|
||||
<>
|
||||
<Typography component="p">{title}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
mb={
|
||||
labelSpacing !== undefined ? theme.spacing(labelSpacing) : theme.spacing(2)
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="h6"
|
||||
mt={theme.spacing(1)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ListItem,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Stack,
|
||||
Typography,
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
@@ -12,6 +11,7 @@ import { useTheme } from "@emotion/react";
|
||||
import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FieldWrapper from "../FieldWrapper";
|
||||
|
||||
/**
|
||||
* Search component using Material UI's Autocomplete.
|
||||
@@ -24,6 +24,7 @@ import { useTranslation } from "react-i18next";
|
||||
* @param {Function} props.handleChange - Function to call when the input changes
|
||||
* @param {Function} Prop.onBlur - Function to call when the input is blured
|
||||
* @param {Object} props.sx - Additional styles to apply to the component
|
||||
* @param {string} props.unit - Label to identify type of options
|
||||
* @returns {JSX.Element} The rendered Search component
|
||||
*/
|
||||
|
||||
@@ -31,7 +32,6 @@ const SearchAdornment = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
mr={theme.spacing(4)}
|
||||
height={16}
|
||||
sx={{
|
||||
"& svg": {
|
||||
@@ -49,7 +49,7 @@ const SearchAdornment = () => {
|
||||
);
|
||||
};
|
||||
|
||||
//TODO keep search state inside of component
|
||||
//TODO keep search state inside of component.
|
||||
const Search = ({
|
||||
label,
|
||||
id,
|
||||
@@ -68,6 +68,14 @@ const Search = ({
|
||||
startAdornment,
|
||||
endAdornment,
|
||||
onBlur,
|
||||
//FieldWrapper's props
|
||||
gap,
|
||||
labelMb,
|
||||
labelFontWeight,
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
unit = "option",
|
||||
maxWidth = "100%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -139,15 +147,17 @@ const Search = ({
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
||||
renderInput={(params) => (
|
||||
<Stack>
|
||||
<Typography
|
||||
component="h3"
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={500}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<FieldWrapper
|
||||
label={label}
|
||||
labelMb={labelMb}
|
||||
labelVariant={labelVariant}
|
||||
labelFontWeight={labelFontWeight}
|
||||
labelSx={labelSx}
|
||||
gap={gap}
|
||||
sx={{
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
{...params}
|
||||
error={Boolean(error)}
|
||||
@@ -175,7 +185,7 @@ const Search = ({
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</FieldWrapper>
|
||||
)}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
if (inputValue.trim() === "" && multiple && isAdorned) {
|
||||
@@ -186,7 +196,12 @@ const Search = ({
|
||||
);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return [{ [filteredBy]: "No monitors found", noOptions: true }];
|
||||
return [
|
||||
{
|
||||
[filteredBy]: t("general.noOptionsFound", { unit: unit }),
|
||||
noOptions: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
return filtered;
|
||||
}}
|
||||
@@ -267,7 +282,9 @@ const Search = ({
|
||||
}}
|
||||
sx={{
|
||||
/* height: 34,*/
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
||||
padding: `0 ${theme.spacing(5)}`,
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
@@ -281,7 +298,7 @@ Search.propTypes = {
|
||||
options: PropTypes.array.isRequired,
|
||||
filteredBy: PropTypes.string.isRequired,
|
||||
secondaryLabel: PropTypes.string,
|
||||
value: PropTypes.array,
|
||||
value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
inputValue: PropTypes.string.isRequired,
|
||||
handleInputChange: PropTypes.func.isRequired,
|
||||
handleChange: PropTypes.func,
|
||||
@@ -292,6 +309,7 @@ Search.propTypes = {
|
||||
startAdornment: PropTypes.object,
|
||||
endAdornment: PropTypes.object,
|
||||
onBlur: PropTypes.func,
|
||||
unit: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@@ -2,6 +2,7 @@ import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import FieldWrapper from "../FieldWrapper";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@@ -49,9 +50,16 @@ const Select = ({
|
||||
onChange,
|
||||
onBlur,
|
||||
sx,
|
||||
error = false,
|
||||
name = "",
|
||||
labelControlSpacing = 2,
|
||||
labelControlSpacing = 6,
|
||||
maxWidth,
|
||||
//FieldWrapper's props
|
||||
labelMb,
|
||||
labelFontWeight,
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
fieldWrapperSx = {},
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const itemStyles = {
|
||||
@@ -69,26 +77,24 @@ const Select = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(labelControlSpacing)}
|
||||
className="select-wrapper"
|
||||
<FieldWrapper
|
||||
label={label}
|
||||
labelMb={labelMb}
|
||||
labelVariant={labelVariant}
|
||||
labelFontWeight={labelFontWeight}
|
||||
labelSx={labelSx}
|
||||
gap={labelControlSpacing}
|
||||
sx={{
|
||||
...fieldWrapperSx,
|
||||
}}
|
||||
>
|
||||
{label && (
|
||||
<Typography
|
||||
component="h3"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
<MuiSelect
|
||||
className="select-component"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
displayEmpty
|
||||
error={error}
|
||||
name={name}
|
||||
inputProps={{ id: id }}
|
||||
IconComponent={KeyboardArrowDownIcon}
|
||||
@@ -107,6 +113,13 @@ const Select = ({
|
||||
"& svg path": {
|
||||
fill: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
padding: "0",
|
||||
minHeight: "34px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
lineHeight: 1,
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
renderValue={(selected) => {
|
||||
@@ -151,7 +164,7 @@ const Select = ({
|
||||
</MenuItem>
|
||||
))}
|
||||
</MuiSelect>
|
||||
</Stack>
|
||||
</FieldWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -161,6 +174,7 @@ Select.propTypes = {
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
isHidden: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
|
||||
.isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Stack, TextField, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { forwardRef, useState, cloneElement } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import FieldWrapper from "../FieldWrapper";
|
||||
|
||||
const getSx = (theme, type, maxWidth) => {
|
||||
const sx = {
|
||||
@@ -85,31 +86,43 @@ const TextInput = forwardRef(
|
||||
marginLeft,
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
//FieldWrapper's props
|
||||
gap,
|
||||
labelMb,
|
||||
labelFontWeight,
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
sx = {},
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [fieldType, setFieldType] = useState(type);
|
||||
const theme = useTheme();
|
||||
const labelContent = label && (
|
||||
<>
|
||||
{label}
|
||||
{isRequired && <Required />}
|
||||
{isOptional && <Optional optionalLabel={optionalLabel} />}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
flex={flex}
|
||||
display={hidden ? "none" : ""}
|
||||
marginTop={marginTop}
|
||||
marginRight={marginRight}
|
||||
marginBottom={marginBottom}
|
||||
marginLeft={marginLeft}
|
||||
<FieldWrapper
|
||||
label={labelContent}
|
||||
labelMb={labelMb}
|
||||
labelVariant={labelVariant}
|
||||
labelFontWeight={labelFontWeight}
|
||||
labelSx={labelSx}
|
||||
gap={gap}
|
||||
sx={{
|
||||
flex,
|
||||
display: hidden ? "none" : "",
|
||||
mt: marginTop,
|
||||
mr: marginRight,
|
||||
mb: marginBottom,
|
||||
ml: marginLeft,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="h3"
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={500}
|
||||
mb={theme.spacing(2)}
|
||||
>
|
||||
{label}
|
||||
{isRequired && <Required />}
|
||||
{isOptional && <Optional optionalLabel={optionalLabel} />}
|
||||
</Typography>
|
||||
<TextField
|
||||
id={id}
|
||||
name={name}
|
||||
@@ -132,7 +145,7 @@ const TextInput = forwardRef(
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</FieldWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,6 +6,13 @@ import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setLanguage } from "../Features/UI/uiSlice";
|
||||
|
||||
const langMap = {
|
||||
cs: "cz",
|
||||
ja: "jp",
|
||||
uk: "ua",
|
||||
vi: "vn",
|
||||
};
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@@ -23,30 +30,44 @@ const LanguageSelector = () => {
|
||||
value={language}
|
||||
onChange={handleChange}
|
||||
size="small"
|
||||
sx={{ minWidth: 80 }}
|
||||
sx={{
|
||||
minWidth: 80,
|
||||
"& .MuiSelect-select": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
"& .MuiSelect-icon": {
|
||||
alignSelf: "center",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{languages.map((lang) => {
|
||||
let parsedLang = lang === "en" ? "gb" : lang;
|
||||
|
||||
// Fix for Czech
|
||||
if (parsedLang === "cs") {
|
||||
parsedLang = "cz";
|
||||
}
|
||||
if (parsedLang.includes("-")) {
|
||||
parsedLang = parsedLang.split("-")[1].toLowerCase();
|
||||
}
|
||||
|
||||
parsedLang = langMap[parsedLang] || parsedLang;
|
||||
|
||||
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={lang}
|
||||
value={lang}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
}
|
||||
} */
|
||||
|
||||
.home-layout aside {
|
||||
/* .home-layout aside {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
max-width: var(--env-var-side-bar-width);
|
||||
}
|
||||
} */
|
||||
|
||||
.home-layout > div {
|
||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
|
||||
@@ -10,7 +10,18 @@ import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => {
|
||||
const NotificationConfig = ({
|
||||
notifications,
|
||||
setMonitor,
|
||||
setNotifications,
|
||||
//FieldWrapper's props
|
||||
gap,
|
||||
labelMb,
|
||||
labelFontWeight,
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
sx = {},
|
||||
}) => {
|
||||
// Local state
|
||||
const [notificationsSearch, setNotificationsSearch] = useState("");
|
||||
const [selectedNotifications, setSelectedNotifications] = useState([]);
|
||||
@@ -66,6 +77,14 @@ const NotificationConfig = ({ notifications, setMonitor, setNotifications }) =>
|
||||
handleChange={(value) => {
|
||||
handleSearch(value);
|
||||
}}
|
||||
labelMb={labelMb}
|
||||
labelVariant={labelVariant}
|
||||
labelFontWeight={labelFontWeight}
|
||||
labelSx={labelSx}
|
||||
gap={gap}
|
||||
sx={{
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
flex={1}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import GenericFallback from "../GenericFallback";
|
||||
import Fallback from "../Fallback";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
import PropTypes from "prop-types";
|
||||
const PageStateWrapper = ({
|
||||
networkError,
|
||||
isLoading,
|
||||
items,
|
||||
type,
|
||||
fallbackLink,
|
||||
fallbackChildren,
|
||||
children,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = useIsAdmin();
|
||||
const isEmpty = items === null || (Array.isArray(items) && items.length === 0);
|
||||
const hasInitialized = items !== undefined && !(isLoading && isEmpty);
|
||||
const showEmpty = !isLoading && isEmpty;
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
{t("common.toasts.networkError")}
|
||||
</Typography>
|
||||
<Typography>{t("common.toasts.checkConnection")}</Typography>
|
||||
</GenericFallback>
|
||||
);
|
||||
}
|
||||
if (!hasInitialized) {
|
||||
return null;
|
||||
}
|
||||
if (showEmpty) {
|
||||
return (
|
||||
<Fallback
|
||||
type={type}
|
||||
title={t(`${type}.fallback.title`)}
|
||||
checks={t(`${type}.fallback.checks`, { returnObjects: true })}
|
||||
link={fallbackLink}
|
||||
isAdmin={isAdmin}
|
||||
>
|
||||
{fallbackChildren}
|
||||
</Fallback>
|
||||
);
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
PageStateWrapper.propTypes = {
|
||||
networkError: PropTypes.any,
|
||||
isLoading: PropTypes.bool,
|
||||
items: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf([null])]),
|
||||
type: PropTypes.string,
|
||||
fallbackLink: PropTypes.string,
|
||||
fallbackChildren: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default PageStateWrapper;
|
||||
@@ -0,0 +1,277 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Avatar from "../../Avatar";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import DotsVertical from "../../../assets/icons/dots-vertical.svg?react";
|
||||
import LogoutSvg from "../../../assets/icons/logout.svg?react";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { clearAuthState } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const getFilteredAccountMenuItems = (user, items) => {
|
||||
if (!user) return [];
|
||||
|
||||
let filtered = [...items];
|
||||
|
||||
if (user.role?.includes("demo")) {
|
||||
filtered = filtered.filter((item) => item.name !== "Password");
|
||||
}
|
||||
|
||||
if (!user.role?.includes("superadmin")) {
|
||||
filtered = filtered.filter((item) => item.name !== "Team");
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const getRoleDisplayText = (user, t) => {
|
||||
if (!user?.role) return "";
|
||||
|
||||
if (user.role.includes("superadmin")) return t("roles.superAdmin");
|
||||
if (user.role.includes("admin")) return t("roles.admin");
|
||||
if (user.role.includes("user")) return t("roles.teamMember");
|
||||
if (user.role.includes("demo")) return t("roles.demoUser");
|
||||
|
||||
return user.role;
|
||||
};
|
||||
|
||||
const AuthFooter = ({ collapsed, accountMenuItems }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const openPopup = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
dispatch(clearAuthState());
|
||||
navigate("/login");
|
||||
};
|
||||
const renderAccountMenuItems = (user, items) => {
|
||||
const filteredItems = getFilteredAccountMenuItems(user, items);
|
||||
|
||||
return filteredItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
onClick={() => {
|
||||
closePopup();
|
||||
navigate(item.path);
|
||||
}}
|
||||
sx={{
|
||||
gap: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
));
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
height="var(--env-var-side-bar-auth-footer-height)"
|
||||
alignItems="center"
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(8)}
|
||||
gap={theme.spacing(2)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
boxSizing={"border-box"}
|
||||
>
|
||||
<Avatar
|
||||
small={true}
|
||||
onClick={(e) => collapsed && openPopup(e)}
|
||||
sx={{
|
||||
cursor: collapsed ? "pointer" : "default",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(2)}
|
||||
minWidth={0}
|
||||
maxWidth={collapsed ? 0 : "100%"}
|
||||
sx={{
|
||||
opacity: collapsed ? 0 : 1,
|
||||
transition: "opacity 300ms ease, max-width 300ms ease",
|
||||
transitionDelay: collapsed ? "0ms" : "300ms",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
ml={theme.spacing(2)}
|
||||
sx={{
|
||||
maxWidth: "50%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontWeight={500}
|
||||
lineHeight={1}
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontSize={"var(--env-var-font-size-small)"}
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
sx={{ textTransform: "capitalize", opacity: 0.8 }}
|
||||
>
|
||||
{getRoleDisplayText(authState.user, t)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Tooltip
|
||||
title={t("navControls")}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
ml: "50px",
|
||||
"&:focus": { outline: "none" },
|
||||
alignSelf: "center",
|
||||
|
||||
"& svg": {
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
},
|
||||
"& svg path": {
|
||||
/* Vertical three dots */
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={(event) => openPopup(event)}
|
||||
>
|
||||
<DotsVertical />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Menu
|
||||
className="sidebar-popup"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={closePopup}
|
||||
disableScrollLock
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
marginTop: theme.spacing(-4),
|
||||
marginLeft: collapsed ? theme.spacing(2) : 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
MenuListProps={{
|
||||
sx: {
|
||||
p: 2,
|
||||
"& li": { m: 0 },
|
||||
"& li:has(.MuiBox-root):hover": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{collapsed && (
|
||||
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
|
||||
<Box
|
||||
mb={theme.spacing(2)}
|
||||
sx={{
|
||||
minWidth: "50%",
|
||||
maxWidth: "max-content",
|
||||
overflow: "visible",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
textOverflow: "clip",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
fontSize: 12,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{authState.user?.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* TODO Do we need two dividers? */}
|
||||
{collapsed && <Divider />}
|
||||
{/* <Divider /> */}
|
||||
{renderAccountMenuItems(authState.user, accountMenuItems)}
|
||||
<MenuItem
|
||||
onClick={logout}
|
||||
sx={{
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
"& svg path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoutSvg />
|
||||
{t("menu.logOut", "Log out")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
AuthFooter.propTypes = {
|
||||
collapsed: PropTypes.bool,
|
||||
accountMenuItems: PropTypes.array,
|
||||
};
|
||||
|
||||
export default AuthFooter;
|
||||
@@ -0,0 +1,55 @@
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ArrowRight from "../../ArrowRight";
|
||||
import ArrowLeft from "../../ArrowLeft";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { toggleSidebar } from "../../../Features/UI/uiSlice";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const CollapseButton = ({ collapsed }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const arrowIcon = collapsed ? (
|
||||
<ArrowRight
|
||||
height={theme.spacing(8)}
|
||||
width={theme.spacing(8)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
/>
|
||||
) : (
|
||||
<ArrowLeft
|
||||
height={theme.spacing(8)}
|
||||
width={theme.spacing(8)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
/* TODO 60 is a magic number. if logo chnges size this might break */
|
||||
top: 60,
|
||||
right: 0,
|
||||
transform: `translate(50%, 0)`,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
p: theme.spacing(2.5),
|
||||
|
||||
"&:focus": { outline: "none" },
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(toggleSidebar());
|
||||
}}
|
||||
>
|
||||
{arrowIcon}
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
CollapseButton.propTypes = {
|
||||
collapsed: PropTypes.bool.isRequired,
|
||||
};
|
||||
export default CollapseButton;
|
||||
@@ -0,0 +1,67 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Logo = ({ collapsed }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
pt={theme.spacing(6)}
|
||||
pb={theme.spacing(12)}
|
||||
pl={theme.spacing(8)}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={() => navigate("/")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Typography
|
||||
pl={theme.spacing("1px")}
|
||||
minWidth={theme.spacing(16)}
|
||||
minHeight={theme.spacing(16)}
|
||||
display={"flex"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
backgroundColor={theme.palette.accent.main}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
color={theme.palette.accent.contrastText}
|
||||
fontSize={18}
|
||||
>
|
||||
C
|
||||
</Typography>
|
||||
<Box
|
||||
overflow={"hidden"}
|
||||
sx={{
|
||||
transition: "opacity 900ms ease, width 900ms ease",
|
||||
opacity: collapsed ? 0 : 1,
|
||||
whiteSpace: "nowrap",
|
||||
width: collapsed ? 0 : "100%",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<Typography
|
||||
lineHeight={1}
|
||||
mt={theme.spacing(2)}
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontSize={"var(--env-var-font-size-medium-plus)"}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Logo.propTypes = {
|
||||
collapsed: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
@@ -0,0 +1,97 @@
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const NavItem = ({ item, collapsed, selected, onClick }) => {
|
||||
const theme = useTheme();
|
||||
const iconStroke = selected
|
||||
? theme.palette.primary.contrastText
|
||||
: theme.palette.primary.contrastTextTertiary;
|
||||
|
||||
const buttonBgColor = selected ? theme.palette.secondary.main : "transparent";
|
||||
const buttonBgHoverColor = selected
|
||||
? theme.palette.secondary.main
|
||||
: theme.palette.tertiary.main;
|
||||
const fontWeight = selected ? 600 : 400;
|
||||
return (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
backgroundColor: buttonBgColor,
|
||||
"&:hover": {
|
||||
backgroundColor: buttonBgHoverColor,
|
||||
},
|
||||
height: 37,
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
pl: theme.spacing(5),
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
"& svg": {
|
||||
height: 20,
|
||||
width: 20,
|
||||
opacity: 0.81,
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: iconStroke,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
transition: "opacity 900ms ease",
|
||||
opacity: collapsed ? 0 : 1,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
sx={{
|
||||
fontWeight: fontWeight,
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
NavItem.propTypes = {
|
||||
item: PropTypes.object,
|
||||
collapsed: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
export default NavItem;
|
||||
@@ -1,106 +0,0 @@
|
||||
/* TODO */
|
||||
aside .MuiList-root svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
aside span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
line-height: 1;
|
||||
}
|
||||
aside .MuiStack-root + span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium-plus);
|
||||
}
|
||||
aside .MuiListSubheader-root {
|
||||
font-size: var(--env-var-font-size-small);
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
aside p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small);
|
||||
opacity: 0.8;
|
||||
}
|
||||
aside .MuiListItemButton-root:not(.selected-path) > * {
|
||||
opacity: 0.9;
|
||||
}
|
||||
aside .selected-path > * {
|
||||
opacity: 1;
|
||||
}
|
||||
aside .selected-path span.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root {
|
||||
position: relative;
|
||||
}
|
||||
aside .MuiCollapse-wrapperInner .MuiList-root svg,
|
||||
aside .MuiList-root .MuiListItemText-root + svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.sidebar-popup svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* TRANSITIONS */
|
||||
aside {
|
||||
flex: 1;
|
||||
transition: max-width 650ms cubic-bezier(0.36, -0.01, 0, 0.77);
|
||||
}
|
||||
.home-layout aside.collapsed {
|
||||
max-width: 64px;
|
||||
}
|
||||
|
||||
aside.expanded .MuiTypography-root,
|
||||
aside.expanded p.MuiTypography-root,
|
||||
aside.expanded .MuiListItemText-root + svg,
|
||||
aside.expanded .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
|
||||
visibility: visible;
|
||||
animation: fadeIn 1s ease;
|
||||
}
|
||||
|
||||
aside.collapsed .MuiTypography-root,
|
||||
aside.collapsed p.MuiTypography-root,
|
||||
aside.collapsed .MuiListItemText-root + svg,
|
||||
aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
aside .MuiListSubheader-root {
|
||||
transition: padding 200ms ease;
|
||||
}
|
||||
|
||||
.sidebar-delay-fade {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
aside.expanded.sidebar-ready .sidebar-delay-fade {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
30% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,16 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
Divider,
|
||||
IconButton,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import ThemeSwitch from "../ThemeSwitch";
|
||||
import Avatar from "../Avatar";
|
||||
import Stack from "@mui/material/Stack";
|
||||
|
||||
import List from "@mui/material/List";
|
||||
import Logo from "./components/logo";
|
||||
import CollapseButton from "./components/collapseButton";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import NavItem from "./components/navItem";
|
||||
import AuthFooter from "./components/authFooter";
|
||||
|
||||
import StarPrompt from "../StarPrompt";
|
||||
import LockSvg from "../../assets/icons/lock.svg?react";
|
||||
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 Maintenance from "../../assets/icons/maintenance.svg?react";
|
||||
import Monitors from "../../assets/icons/monitors.svg?react";
|
||||
@@ -28,11 +18,6 @@ import Incidents from "../../assets/icons/incidents.svg?react";
|
||||
import Integrations from "../../assets/icons/integrations.svg?react";
|
||||
import PageSpeed from "../../assets/icons/page-speed.svg?react";
|
||||
import Settings from "../../assets/icons/settings.svg?react";
|
||||
import ArrowDown from "../../assets/icons/down-arrow.svg?react";
|
||||
import ArrowUp from "../../assets/icons/up-arrow.svg?react";
|
||||
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
|
||||
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 StatusPages from "../../assets/icons/status-pages.svg?react";
|
||||
@@ -40,17 +25,18 @@ import Discussions from "../../assets/icons/discussions.svg?react";
|
||||
import Notifications from "../../assets/icons/notifications.svg?react";
|
||||
import Logs from "../../assets/icons/logs.svg?react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
// Utils
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { toggleSidebar } from "../../Features/UI/uiSlice";
|
||||
import { TurnedIn } from "@mui/icons-material";
|
||||
import { rules } from "eslint-plugin-react-refresh";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
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/checkmate/releases",
|
||||
};
|
||||
|
||||
const getMenu = (t) => [
|
||||
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
|
||||
@@ -92,782 +78,94 @@ const getAccountMenuItems = (t) => [
|
||||
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
|
||||
];
|
||||
|
||||
/* TODO this could be a key in nested Path would be the link */
|
||||
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/checkmate/releases",
|
||||
};
|
||||
|
||||
const PATH_MAP = {
|
||||
monitors: "Dashboard",
|
||||
pagespeed: "Dashboard",
|
||||
infrastructure: "Dashboard",
|
||||
account: "Account",
|
||||
settings: "Settings",
|
||||
};
|
||||
|
||||
/**
|
||||
* @component
|
||||
* Sidebar component serves as a sidebar containing a menu.
|
||||
*
|
||||
* @returns {JSX.Element} The JSX element representing the Sidebar component.
|
||||
*/
|
||||
|
||||
function Sidebar() {
|
||||
const Sidebar = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const navigate = useNavigate();
|
||||
// Redux state
|
||||
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
|
||||
|
||||
const menu = getMenu(t);
|
||||
const otherMenuItems = getOtherMenuItems(t);
|
||||
const accountMenuItems = getAccountMenuItems(t);
|
||||
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
|
||||
const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false });
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [popup, setPopup] = useState();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
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) {
|
||||
setSidebarReady(false);
|
||||
const timeout = setTimeout(() => {
|
||||
setSidebarReady(true);
|
||||
}, TRANSITION_DURATION);
|
||||
return () => clearTimeout(timeout);
|
||||
} else {
|
||||
setSidebarReady(false);
|
||||
}
|
||||
}, [collapsed]);
|
||||
|
||||
const renderAccountMenuItems = () => {
|
||||
let filteredAccountMenuItems = [...accountMenuItems];
|
||||
|
||||
// If the user is in demo mode, remove the "Password" option
|
||||
if (user.role?.includes("demo")) {
|
||||
filteredAccountMenuItems = filteredAccountMenuItems.filter(
|
||||
(item) => item.name !== "Password"
|
||||
);
|
||||
}
|
||||
|
||||
// If the user is NOT a superadmin, remove the "Team" option
|
||||
if (user.role && !user.role.includes("superadmin")) {
|
||||
filteredAccountMenuItems = filteredAccountMenuItems.filter(
|
||||
(item) => item.name !== "Team"
|
||||
);
|
||||
}
|
||||
|
||||
return filteredAccountMenuItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
onClick={() => {
|
||||
closePopup();
|
||||
navigate(item.path);
|
||||
}}
|
||||
sx={{
|
||||
gap: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
));
|
||||
};
|
||||
|
||||
const openPopup = (event, id) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setPopup(id);
|
||||
};
|
||||
const closePopup = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles logging out the user
|
||||
*
|
||||
*/
|
||||
const logout = async () => {
|
||||
// Clear auth state
|
||||
dispatch(clearAuthState());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const matchedKey = Object.keys(PATH_MAP).find((key) =>
|
||||
location.pathname.includes(key)
|
||||
);
|
||||
|
||||
if (matchedKey) {
|
||||
setOpen((prev) => ({ ...prev, [PATH_MAP[matchedKey]]: true }));
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
const iconColor = theme.palette.primary.contrastTextTertiary;
|
||||
const sidebarClassName = `${collapsed ? "collapsed" : "expanded"} ${sidebarReady ? "sidebar-ready" : ""}`;
|
||||
|
||||
/* TODO refactor this, there are a some ternaries and comments in the return */
|
||||
return (
|
||||
<Stack
|
||||
height="100vh"
|
||||
width={
|
||||
collapsed
|
||||
? "var(--env-var-side-bar-collapsed-width)"
|
||||
: "var(--env-var-side-bar-width)"
|
||||
}
|
||||
component="aside"
|
||||
ref={sidebarRef}
|
||||
className={sidebarClassName}
|
||||
/* TODO general padding should be here */
|
||||
py={theme.spacing(6)}
|
||||
position="sticky"
|
||||
top={0}
|
||||
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
paddingTop={theme.spacing(6)}
|
||||
paddingBottom={theme.spacing(6)}
|
||||
gap={theme.spacing(6)}
|
||||
/* TODO set all style in this sx if possible (when general)
|
||||
This is the top lever for styles
|
||||
*/
|
||||
sx={{
|
||||
position: "relative",
|
||||
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: 0,
|
||||
"& :is(p, span, .MuiListSubheader-root)": {
|
||||
/*
|
||||
Text color for unselected menu items and menu headings
|
||||
Secondary contrast text against main background
|
||||
*/
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
},
|
||||
"& .MuiList-root svg path": {
|
||||
/* Menu Icons */
|
||||
stroke: iconColor,
|
||||
},
|
||||
"& .selected-path": {
|
||||
/* Selected menu item */
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
"& .MuiListItemIcon-root svg path": {
|
||||
/* Selected menu item icon */
|
||||
stroke: theme.palette.secondary.contrastText,
|
||||
},
|
||||
"& .MuiListItemText-root :is(p, span)": {
|
||||
/* Selected menu item text */
|
||||
color: theme.palette.secondary.contrastText,
|
||||
},
|
||||
},
|
||||
"& .MuiListItemButton-root:not(.selected-path)": {
|
||||
transition: "background-color .3s",
|
||||
" &:hover": {
|
||||
/* Hovered menu item bg color */
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
"& :is(p, span)": {
|
||||
/* Hovered menu item text color */
|
||||
color: theme.palette.tertiary.contrastText,
|
||||
},
|
||||
},
|
||||
},
|
||||
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
<CollapseButton collapsed={collapsed} />
|
||||
<Logo collapsed={collapsed} />
|
||||
<List
|
||||
component="nav"
|
||||
aria-labelledby="nested-menu-subheader"
|
||||
disablePadding
|
||||
sx={{
|
||||
position: "absolute",
|
||||
/* TODO 60 is a magic number. if logo chnges size this might break */
|
||||
top: 60,
|
||||
right: 0,
|
||||
transform: `translate(50%, 0)`,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
p: theme.spacing(2.5),
|
||||
"& svg": {
|
||||
width: theme.spacing(8),
|
||||
height: theme.spacing(8),
|
||||
"& path": {
|
||||
/* TODO this should be set at the top level if possible */
|
||||
stroke: theme.palette.primary.contrastTextSecondary,
|
||||
},
|
||||
},
|
||||
"&:focus": { outline: "none" },
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
px: theme.spacing(6),
|
||||
height: "100%",
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen((prev) =>
|
||||
Object.fromEntries(Object.keys(prev).map((key) => [key, false]))
|
||||
>
|
||||
{menu.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
selected={selected}
|
||||
onClick={() => navigate(`/${item.path}`)}
|
||||
/>
|
||||
);
|
||||
dispatch(toggleSidebar());
|
||||
}}
|
||||
>
|
||||
{collapsed ? <ArrowRight /> : <ArrowLeft />}
|
||||
</IconButton>
|
||||
{/* TODO Alignment done using padding. Use single source of truth to that*/}
|
||||
<Stack
|
||||
pt={theme.spacing(6)}
|
||||
pb={theme.spacing(12)}
|
||||
pl={theme.spacing(8)}
|
||||
>
|
||||
{/* TODO Abstract logo into component */}
|
||||
{/* TODO Turn logo into a link */}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={() => navigate("/")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minWidth={theme.spacing(16)}
|
||||
minHeight={theme.spacing(16)}
|
||||
pl="1px"
|
||||
fontSize={18}
|
||||
color={theme.palette.accent.contrastText}
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: theme.palette.accent.main,
|
||||
color: theme.palette.accent.contrastText,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
C
|
||||
</Stack>
|
||||
<Typography
|
||||
component="span"
|
||||
mt={theme.spacing(2)}
|
||||
sx={{ opacity: 0.8, fontWeight: 500 }}
|
||||
>
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
overflowX: "hidden",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: theme.spacing(2),
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.spacing(4),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<List
|
||||
component="nav"
|
||||
aria-labelledby="nested-menu-subheader"
|
||||
disablePadding
|
||||
sx={{
|
||||
px: theme.spacing(6),
|
||||
height: "100%",
|
||||
/* overflow: "hidden", */
|
||||
}}
|
||||
>
|
||||
{menu.map((item) => {
|
||||
return item.path ? (
|
||||
/* If item has a path */
|
||||
<Tooltip
|
||||
key={item.path}
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
className={
|
||||
location.pathname.startsWith(`/${item.path}`) ? "selected-path" : ""
|
||||
}
|
||||
onClick={() => navigate(`/${item.path}`)}
|
||||
sx={{
|
||||
height: "37px",
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
pl: theme.spacing(5),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
|
||||
<ListItemText>{item.name}</ListItemText>
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
) : collapsed ? (
|
||||
/* TODO Do we ever get here? If item does not have a path and collapsed state is true */
|
||||
<React.Fragment key={item.name}>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
className={
|
||||
Boolean(anchorEl) && popup === item.name ? "selected-path" : ""
|
||||
}
|
||||
onClick={(event) => openPopup(event, item.name)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
|
||||
<ListItemText>{item.name}</ListItemText>
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
className="sidebar-popup"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl) && popup === item.name}
|
||||
onClose={closePopup}
|
||||
disableScrollLock
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
mt: theme.spacing(-2),
|
||||
ml: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}}
|
||||
MenuListProps={{ sx: { px: 1, py: 2 } }}
|
||||
sx={{
|
||||
ml: theme.spacing(8),
|
||||
/* TODO what is this selection? */
|
||||
"& .selected-path": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.nested.map((child) => {
|
||||
if (
|
||||
child.name === "Team" &&
|
||||
authState.user?.role &&
|
||||
!authState.user.role.includes("superadmin")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={
|
||||
location.pathname.includes(child.path) ? "selected-path" : ""
|
||||
}
|
||||
key={child.path}
|
||||
onClick={() => {
|
||||
const url = URL_MAP[child.path];
|
||||
if (url) {
|
||||
window.open(url, "_blank", "noreferrer");
|
||||
} else {
|
||||
navigate(`/${child.path}`);
|
||||
}
|
||||
closePopup();
|
||||
}}
|
||||
sx={{
|
||||
gap: theme.spacing(4),
|
||||
opacity: 0.9,
|
||||
/* TODO this has no effect? */
|
||||
"& svg": {
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 1.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{child.icon}
|
||||
{child.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
/* TODO Do we ever get here? If item does not have a path and collapsed state is false */
|
||||
<React.Fragment key={item.name}>
|
||||
<ListItemButton
|
||||
onClick={() =>
|
||||
setOpen((prev) => ({
|
||||
...Object.fromEntries(Object.keys(prev).map((key) => [key, false])),
|
||||
[item.name]: !prev[item.name],
|
||||
}))
|
||||
}
|
||||
sx={{
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
|
||||
<ListItemText>{item.name}</ListItemText>
|
||||
{open[`${item.name}`] ? <ArrowUp /> : <ArrowDown />}
|
||||
</ListItemButton>
|
||||
<Collapse
|
||||
in={open[`${item.name}`]}
|
||||
timeout="auto"
|
||||
>
|
||||
<List
|
||||
component="div"
|
||||
disablePadding
|
||||
sx={{ pl: theme.spacing(12) }}
|
||||
>
|
||||
{item.nested.map((child) => {
|
||||
if (
|
||||
child.name === "Team" &&
|
||||
authState.user?.role &&
|
||||
!authState.user.role.includes("superadmin")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
className={
|
||||
location.pathname.includes(child.path) ? "selected-path" : ""
|
||||
}
|
||||
key={child.path}
|
||||
onClick={() => {
|
||||
const url = URL_MAP[child.path];
|
||||
if (url) {
|
||||
window.open(url, "_blank", "noreferrer");
|
||||
} else {
|
||||
navigate(`/${child.path}`);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
"&::before": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "-7px",
|
||||
height: "100%",
|
||||
borderLeft: 1,
|
||||
borderLeftColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&:last-child::before": {
|
||||
height: "50%",
|
||||
},
|
||||
"&::after": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
top: "45%",
|
||||
left: "-8px",
|
||||
height: "3px",
|
||||
width: "3px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&.selected-path::after": {
|
||||
/* TODO what is this selector doing? */
|
||||
backgroundColor: theme.palette.primary.contrastTextTertiary,
|
||||
transform: "scale(1.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{child.icon}</ListItemIcon>
|
||||
<ListItemText>{child.name}</ListItemText>
|
||||
</ListItemButton>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Collapse>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
})}
|
||||
</List>
|
||||
{!collapsed && <StarPrompt />}
|
||||
|
||||
<List
|
||||
component="nav"
|
||||
disablePadding
|
||||
sx={{ px: theme.spacing(6) }}
|
||||
>
|
||||
{otherMenuItems.map((item) => {
|
||||
return item.path ? (
|
||||
<Tooltip
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
className={
|
||||
location.pathname.startsWith(`/${item.path}`) ? "selected-path" : ""
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
selected={selected}
|
||||
onClick={() => {
|
||||
const url = URL_MAP[item.path];
|
||||
if (url) {
|
||||
window.open(url, "_blank", "noreferrer");
|
||||
} else {
|
||||
navigate(`/${item.path}`);
|
||||
}
|
||||
onClick={() => {
|
||||
const url = URL_MAP[item.path];
|
||||
if (url) {
|
||||
window.open(url, "_blank", "noreferrer");
|
||||
} else {
|
||||
navigate(`/${item.path}`);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
height: "37px",
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
pl: theme.spacing(5),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon} </ListItemIcon>
|
||||
<ListItemText>{item.name} </ListItemText>{" "}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
|
||||
<Stack
|
||||
direction="row"
|
||||
height="50px"
|
||||
alignItems="center"
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(8)}
|
||||
gap={theme.spacing(2)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
>
|
||||
{collapsed ? (
|
||||
<>
|
||||
<Tooltip
|
||||
title="Options"
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -10],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
onClick={(event) => openPopup(event, "logout")}
|
||||
sx={{ p: 0, "&:focus": { outline: "none" } }}
|
||||
>
|
||||
<Avatar small={true} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Avatar small={true} />
|
||||
<Box
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ maxWidth: "50%", overflow: "hidden" }}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight={500}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{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}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
className="sidebar-delay-fade"
|
||||
flexDirection={"row"}
|
||||
marginLeft={"auto"}
|
||||
columnGap={theme.spacing(2)}
|
||||
>
|
||||
<ThemeSwitch color={iconColor} />
|
||||
<Tooltip
|
||||
title={t("navControls")}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
ml: "auto",
|
||||
mr: "-8px",
|
||||
"&:focus": { outline: "none" },
|
||||
alignSelf: "center",
|
||||
padding: "10px",
|
||||
|
||||
"& svg": {
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
},
|
||||
"& svg path": {
|
||||
/* Vertical three dots */
|
||||
stroke: iconColor,
|
||||
},
|
||||
}}
|
||||
onClick={(event) => openPopup(event, "logout")}
|
||||
>
|
||||
<DotsVertical />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Menu
|
||||
className="sidebar-popup"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl) && popup === "logout"}
|
||||
onClose={closePopup}
|
||||
disableScrollLock
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
marginTop: theme.spacing(-4),
|
||||
marginLeft: collapsed ? theme.spacing(2) : 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
MenuListProps={{
|
||||
sx: {
|
||||
p: 2,
|
||||
"& li": { m: 0 },
|
||||
"& li:has(.MuiBox-root):hover": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{collapsed && (
|
||||
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
|
||||
<Box
|
||||
mb={theme.spacing(2)}
|
||||
sx={{
|
||||
minWidth: "50%",
|
||||
maxWidth: "max-content",
|
||||
overflow: "visible",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
textOverflow: "clip",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
fontSize: 12,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{authState.user?.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* TODO Do we need two dividers? */}
|
||||
{collapsed && <Divider />}
|
||||
{/* <Divider /> */}
|
||||
{renderAccountMenuItems()}
|
||||
<MenuItem
|
||||
onClick={logout}
|
||||
sx={{
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
"& svg path": {
|
||||
stroke: iconColor,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoutSvg />
|
||||
{t("menu.logOut", "Log out")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
<AuthFooter
|
||||
collapsed={collapsed}
|
||||
accountMenuItems={accountMenuItems}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -74,7 +74,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: "0.938rem",
|
||||
lineHeight: 1.5,
|
||||
mb: 1,
|
||||
|
||||
@@ -16,6 +16,7 @@ const CustomTabList = ({ value, onChange, children, ...props }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
marginBottom: theme.spacing(12),
|
||||
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
|
||||
}}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import LeftArrowDouble from "../../../../assets/icons/left-arrow-double.svg?react";
|
||||
import RightArrowDouble from "../../../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
|
||||
import LeftArrow from "../../../ArrowLeft";
|
||||
import RightArrow from "../../../ArrowRight";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
TablePaginationActions.propTypes = {
|
||||
@@ -50,7 +48,7 @@ function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
<LeftArrow type="double" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
@@ -74,7 +72,7 @@ function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
<RightArrowDouble />
|
||||
<RightArrow type="double" />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -41,6 +42,7 @@ const DataTable = ({
|
||||
data = [],
|
||||
config = {
|
||||
emptyView: "No data",
|
||||
tooltipContent: null,
|
||||
onRowClick: () => {},
|
||||
},
|
||||
}) => {
|
||||
@@ -70,6 +72,9 @@ const DataTable = ({
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
},
|
||||
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
@@ -98,24 +103,54 @@ const DataTable = ({
|
||||
data.map((row) => {
|
||||
const key = row.id || row._id || Math.random();
|
||||
return (
|
||||
<TableRow
|
||||
<Tooltip
|
||||
key={key}
|
||||
sx={config?.rowSX ?? {}}
|
||||
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
|
||||
followCursor
|
||||
enterDelay={500}
|
||||
enterNextDelay={500}
|
||||
title={
|
||||
typeof config.tooltipContent === "function"
|
||||
? config.tooltipContent(row)
|
||||
: config.tooltipContent
|
||||
}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
background: "unset",
|
||||
},
|
||||
},
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: ({ popper }) => {
|
||||
return [popper.width / 2 + 20, -popper.height / 8];
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
{headers.map((header, index) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={index === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
onClick={header.onClick ? (e) => header.onClick(e, row) : null}
|
||||
sx={header.getCellSx ? header.getCellSx(row) : {}}
|
||||
>
|
||||
{header.render(row)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={config?.rowSX ?? {}}
|
||||
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
|
||||
>
|
||||
{headers.map((header, index) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={index === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
onClick={header.onClick ? (e) => header.onClick(e, row) : null}
|
||||
sx={header.getCellSx ? header.getCellSx(row) : {}}
|
||||
>
|
||||
{header.render(row)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ToastBody = ({ body }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (Array.isArray(body)) {
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
{body.map((item, idx) => (
|
||||
<Typography
|
||||
key={`item-${idx}`}
|
||||
color={theme.palette.secondary.contrastText}
|
||||
>
|
||||
{item}
|
||||
</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
} else if (typeof body === "string") {
|
||||
return <Typography color={theme.palette.secondary.contrastText}>{body}</Typography>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ToastBody.propTypes = {
|
||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
};
|
||||
|
||||
export default ToastBody;
|
||||
@@ -0,0 +1,96 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import ToastBody from "./body";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const icons = {
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
};
|
||||
|
||||
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
|
||||
const theme = useTheme();
|
||||
const icon = icons[variant];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
paddingTop={theme.spacing(4)}
|
||||
paddingRight={theme.spacing(8)}
|
||||
paddingBottom={theme.spacing(4)}
|
||||
paddingLeft={theme.spacing(8)}
|
||||
backgroundColor={theme.palette.alert.main}
|
||||
border={`solid 1px ${theme.palette.alert.contrastText}`}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
{hasIcon && icon}
|
||||
{title && (
|
||||
<Typography
|
||||
fontWeight="700"
|
||||
color={theme.palette.secondary.contrastText}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{title && (
|
||||
<IconButton onClick={onClick}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
>
|
||||
<ToastBody body={body} />
|
||||
{!title && (
|
||||
<IconButton onClick={onClick}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
{hasDismiss && (
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
fontWeight: "600",
|
||||
width: "fit-content",
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
||||
|
||||
Toast.propTypes = {
|
||||
variant: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
hasDismiss: PropTypes.bool,
|
||||
hasIcon: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
@@ -44,7 +44,6 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||
|
||||
export const update = createAsyncThunk("auth/update", async (data, thunkApi) => {
|
||||
const { localData: form } = data;
|
||||
const user = thunkApi.getState().auth.user;
|
||||
try {
|
||||
const fd = new FormData();
|
||||
form.firstName && fd.append("firstName", form.firstName);
|
||||
@@ -60,7 +59,6 @@ export const update = createAsyncThunk("auth/update", async (data, thunkApi) =>
|
||||
form.deleteProfileImage && fd.append("deleteProfileImage", form.deleteProfileImage);
|
||||
|
||||
const res = await networkService.updateUser({
|
||||
userId: user._id,
|
||||
form: fd,
|
||||
});
|
||||
return res.data;
|
||||
@@ -77,10 +75,8 @@ export const update = createAsyncThunk("auth/update", async (data, thunkApi) =>
|
||||
});
|
||||
|
||||
export const deleteUser = createAsyncThunk("auth/delete", async (_, thunkApi) => {
|
||||
const user = thunkApi.getState().auth.user;
|
||||
|
||||
try {
|
||||
const res = await networkService.deleteUser({ userId: user._id });
|
||||
const res = await networkService.deleteUser();
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
|
||||
@@ -185,23 +185,30 @@ const useResolveIncident = () => {
|
||||
return [resolveIncident, isLoading];
|
||||
};
|
||||
|
||||
const useAckAllChecks = () => {
|
||||
const useAcknowledgeChecks = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ackAllChecks = async (setUpdateTrigger) => {
|
||||
const acknowledge = async (setUpdateTrigger, monitorId = null) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.updateAllChecksStatus({ ack: true });
|
||||
if (monitorId) {
|
||||
await networkService.updateMonitorChecksStatus({ monitorId, ack: true });
|
||||
} else {
|
||||
await networkService.updateAllChecksStatus({ ack: true });
|
||||
}
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
} catch (error) {
|
||||
createToast({ body: t("checkHooks.failureResolveAll") });
|
||||
const toastMessage = monitorId
|
||||
? t("checkHooks.failureResolveMonitor")
|
||||
: t("checkHooks.failureResolveAll");
|
||||
createToast({ body: toastMessage });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [ackAllChecks, isLoading];
|
||||
return { acknowledge, isLoading };
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -209,5 +216,5 @@ export {
|
||||
useFetchChecksTeam,
|
||||
useFetchChecksSummaryByTeamId,
|
||||
useResolveIncident,
|
||||
useAckAllChecks,
|
||||
useAcknowledgeChecks,
|
||||
};
|
||||
@@ -14,25 +14,25 @@ const useGetInviteToken = () => {
|
||||
const clearToken = () => {
|
||||
setToken(undefined);
|
||||
};
|
||||
|
||||
const fetchToken = async (email, role) => {
|
||||
const response = await networkService.requestInvitationToken({ email, role });
|
||||
const token = response?.data?.data?.token;
|
||||
if (typeof token === "undefined") {
|
||||
throw new Error(t("inviteNoTokenFound"));
|
||||
}
|
||||
return token;
|
||||
};
|
||||
const getInviteToken = async ({ email, role }) => {
|
||||
try {
|
||||
const response = await networkService.requestInvitationToken({
|
||||
email,
|
||||
role,
|
||||
});
|
||||
const token = response?.data?.data?.token;
|
||||
if (typeof token === "undefined") {
|
||||
throw new Error(t("inviteNoTokenFound"));
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const token = await fetchToken(email, role);
|
||||
let inviteLink = token;
|
||||
|
||||
if (typeof CLIENT_HOST !== "undefined") {
|
||||
inviteLink = `${CLIENT_HOST}/register/${token}`;
|
||||
}
|
||||
|
||||
setToken(inviteLink);
|
||||
return token;
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
@@ -40,7 +40,26 @@ const useGetInviteToken = () => {
|
||||
}
|
||||
};
|
||||
|
||||
return [getInviteToken, clearToken, isLoading, error, token];
|
||||
const addTeamMember = async (formData, role) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const token = await fetchToken(formData.email, role);
|
||||
const toSubmit = {
|
||||
...formData,
|
||||
inviteToken: token,
|
||||
};
|
||||
delete toSubmit.confirm;
|
||||
const responseRegister = await networkService.registerUser(toSubmit);
|
||||
return responseRegister;
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [getInviteToken, clearToken, isLoading, error, token, addTeamMember];
|
||||
};
|
||||
|
||||
export { useGetInviteToken };
|
||||
@@ -15,9 +15,6 @@ const useFetchLogs = () => {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getLogs();
|
||||
setLogs(response.data.data);
|
||||
createToast({
|
||||
body: t("logsPage.toast.fetchLogsSuccess"),
|
||||
});
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
createToast({
|
||||
@@ -32,4 +29,83 @@ const useFetchLogs = () => {
|
||||
return [logs, isLoading, error];
|
||||
};
|
||||
|
||||
export { useFetchLogs };
|
||||
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];
|
||||
};
|
||||
|
||||
const useFetchDiagnostics = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
const [diagnostics, setDiagnostics] = useState(undefined);
|
||||
|
||||
const fetchDiagnostics = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getDiagnostics();
|
||||
setDiagnostics(response.data.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDiagnostics();
|
||||
}, []);
|
||||
|
||||
return [diagnostics, fetchDiagnostics, isLoading, error];
|
||||
};
|
||||
|
||||
export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics };
|
||||
@@ -202,6 +202,25 @@ const useFetchStatsByMonitorId = ({
|
||||
return [monitor, audits, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
const fetchGames = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getMonitorGames();
|
||||
setGames(res.data.data);
|
||||
} catch (error) {
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchGames();
|
||||
}, [setGames, updateTrigger]);
|
||||
return [isLoading];
|
||||
};
|
||||
|
||||
const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
@@ -300,6 +319,28 @@ const useCreateMonitor = () => {
|
||||
return [createMonitor, isLoading];
|
||||
};
|
||||
|
||||
const useFetchGlobalSettings = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [globalSettings, setGlobalSettings] = useState(undefined);
|
||||
useEffect(() => {
|
||||
const fetchGlobalSettings = async () => {
|
||||
try {
|
||||
const res = await networkService.getAppSettings();
|
||||
setGlobalSettings(res?.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch global settings:", error);
|
||||
createToast({ body: "Failed to load global settings" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchGlobalSettings();
|
||||
}, []);
|
||||
|
||||
return [globalSettings, isLoading];
|
||||
};
|
||||
|
||||
const useDeleteMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@@ -328,6 +369,8 @@ const useUpdateMonitor = () => {
|
||||
setIsLoading(true);
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
statusWindowSize: monitor.statusWindowSize,
|
||||
statusWindowThreshold: monitor.statusWindowThreshold,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
@@ -335,7 +378,12 @@ const useUpdateMonitor = () => {
|
||||
expectedValue: monitor.expectedValue,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
jsonPath: monitor.jsonPath,
|
||||
...(monitor.type === "port" && { port: monitor.port }),
|
||||
...((monitor.type === "port" || monitor.type === "game") && {
|
||||
port: monitor.port,
|
||||
}),
|
||||
...(monitor.type == "game" && {
|
||||
gameId: monitor.gameId,
|
||||
}),
|
||||
...(monitor.type === "hardware" && {
|
||||
thresholds: monitor.thresholds,
|
||||
secret: monitor.secret,
|
||||
@@ -499,6 +547,7 @@ export {
|
||||
useFetchUptimeMonitorById,
|
||||
useFetchHardwareMonitorById,
|
||||
useCreateMonitor,
|
||||
useFetchGlobalSettings,
|
||||
useDeleteMonitor,
|
||||
useUpdateMonitor,
|
||||
usePauseMonitor,
|
||||
@@ -507,4 +556,5 @@ export {
|
||||
useDeleteMonitorStats,
|
||||
useCreateBulkMonitors,
|
||||
useExportMonitors,
|
||||
useFetchMonitorGames,
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
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 };
|
||||
@@ -8,4 +8,10 @@ const useIsAdmin = () => {
|
||||
return isAdmin;
|
||||
};
|
||||
|
||||
export { useIsAdmin };
|
||||
const useIsSuperAdmin = () => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const isSuperAdmin = user?.role?.includes("superadmin");
|
||||
return isSuperAdmin;
|
||||
};
|
||||
|
||||
export { useIsAdmin, useIsSuperAdmin };
|
||||
@@ -6,18 +6,18 @@ const useMonitorUtils = () => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = "";
|
||||
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
if (monitor?.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
monitor?.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
: (monitor?.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
monitor?.uptimePercentage < 0.25
|
||||
? theme.palette.error.main
|
||||
: monitor.uptimePercentage < 0.5
|
||||
: monitor?.uptimePercentage < 0.5
|
||||
? theme.palette.warning.main
|
||||
: monitor.uptimePercentage < 0.75
|
||||
: monitor?.uptimePercentage < 0.75
|
||||
? theme.palette.success.main
|
||||
: theme.palette.success.main;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ const useMonitorUtils = () => {
|
||||
|
||||
const determineState = useCallback((monitor) => {
|
||||
if (typeof monitor === "undefined") return "pending";
|
||||
if (monitor.isActive === false) return "paused";
|
||||
if (monitor?.isActive === false) return "paused";
|
||||
if (monitor?.status === undefined) return "pending";
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
}, []);
|
||||
|
||||
@@ -34,7 +34,7 @@ const useCreateNotification = () => {
|
||||
|
||||
const useGetNotificationsByTeamId = (updateTrigger) => {
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const useGetUser = (userId) => {
|
||||
const [user, setUser] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchUser = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getUserById({ userId });
|
||||
setUser(response?.data?.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
fetchUser();
|
||||
}
|
||||
}, [userId, fetchUser]);
|
||||
|
||||
return [user, isLoading, error];
|
||||
};
|
||||
|
||||
export const useEditUser = (userId) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const editUser = useCallback(
|
||||
async (user) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.editUser({ userId, user });
|
||||
createToast({
|
||||
body: t("editUserPage.toast.successUserUpdate"),
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[userId]
|
||||
);
|
||||
|
||||
return [editUser, isLoading, error];
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ROLES } from "../../../../Utils/roleUtils";
|
||||
import { editUserValidation } from "../../../../Validation/validation";
|
||||
import Joi from "joi";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
export const useEditUserForm = (user) => {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [form, setForm] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
role: [],
|
||||
});
|
||||
|
||||
// Effect to set user
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setForm({
|
||||
firstName: user?.firstName,
|
||||
lastName: user?.lastName,
|
||||
email: user?.email,
|
||||
role: user?.role,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleRoleChange = (value) => {
|
||||
const hasSuperAdmin = form.role.includes(ROLES.SUPERADMIN);
|
||||
const newRoles = value.map((item) => item.role);
|
||||
if (hasSuperAdmin && !newRoles.includes(ROLES.SUPERADMIN)) {
|
||||
newRoles.push(ROLES.SUPERADMIN);
|
||||
}
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
role: newRoles,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteRole = (role) => {
|
||||
if (role === ROLES.SUPERADMIN) return;
|
||||
setForm({ ...form, role: form?.role?.filter((r) => r !== role) });
|
||||
};
|
||||
|
||||
const handleSearchInput = (value) => {
|
||||
setSearchInput(value);
|
||||
};
|
||||
|
||||
return [
|
||||
form,
|
||||
setForm,
|
||||
handleRoleChange,
|
||||
handleDeleteRole,
|
||||
searchInput,
|
||||
handleSearchInput,
|
||||
];
|
||||
};
|
||||
|
||||
export const useValidateEditUserForm = () => {
|
||||
const [errors, setErrors] = useState({});
|
||||
const { t } = useTranslation();
|
||||
const validateForm = (form) => {
|
||||
const { error } = editUserValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
const errs = error?.details;
|
||||
const errsObj = errs?.reduce((acc, curr) => {
|
||||
acc[curr.path[0]] = curr.message;
|
||||
return acc;
|
||||
}, {});
|
||||
setErrors(errsObj);
|
||||
|
||||
if (errs?.length > 0) {
|
||||
createToast({
|
||||
variant: "warning",
|
||||
hasIcon: true,
|
||||
title: t("editUserPage.toast.validationErrors"),
|
||||
body: errs.map((err) => t(err.message)),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateField = (name, value) => {
|
||||
const fieldSchema = Joi.object({
|
||||
[name]: editUserValidation.extract(name),
|
||||
});
|
||||
|
||||
const { error } = fieldSchema.validate({ [name]: value });
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
return [errors, validateForm, validateField];
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Search from "../../../Components/Inputs/Search";
|
||||
import Button from "@mui/material/Button";
|
||||
import RoleTable from "../components/RoleTable";
|
||||
|
||||
// Utils
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetUser, useEditUser } from "../../../Hooks/userHooks";
|
||||
import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils";
|
||||
import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser";
|
||||
|
||||
const EditUser = () => {
|
||||
const { userId } = useParams();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const BREADCRUMBS = [
|
||||
{ name: t("menu.team"), path: "/account/team" },
|
||||
{ name: t("editUserPage.title"), path: "" },
|
||||
];
|
||||
|
||||
const [user, isLoading, error] = useGetUser(userId);
|
||||
const [editUser, isSaving, saveError] = useEditUser(userId);
|
||||
const [
|
||||
form,
|
||||
setForm,
|
||||
handleRoleChange,
|
||||
handleDeleteRole,
|
||||
searchInput,
|
||||
handleSearchInput,
|
||||
] = useEditUserForm(user);
|
||||
const [errors, validateForm, validateField] = useValidateEditUserForm();
|
||||
|
||||
const onChange = (e) => {
|
||||
const name = e.target.name;
|
||||
const value = e.target.value;
|
||||
validateField(name, value);
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const valid = validateForm(form);
|
||||
if (valid) {
|
||||
editUser(form);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Typography variant="h2">{t("editUserPage.title")}</Typography>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
gap={theme.spacing(12)}
|
||||
maxWidth="50%"
|
||||
>
|
||||
<TextInput
|
||||
name="firstName"
|
||||
label={t("editUserPage.form.firstName")}
|
||||
value={form?.firstName}
|
||||
onChange={onChange}
|
||||
error={errors?.firstName ? true : false}
|
||||
helperText={t(errors?.firstName)}
|
||||
/>
|
||||
<TextInput
|
||||
name="lastName"
|
||||
label={t("editUserPage.form.lastName")}
|
||||
value={form?.lastName}
|
||||
onChange={onChange}
|
||||
error={errors?.lastName ? true : false}
|
||||
helperText={t(errors?.lastName)}
|
||||
/>
|
||||
<TextInput
|
||||
name="email"
|
||||
label={t("editUserPage.form.email")}
|
||||
value={form?.email}
|
||||
disabled={true}
|
||||
error={errors?.email ? true : false}
|
||||
helperText={t(errors?.email)}
|
||||
/>
|
||||
<Search
|
||||
label={t("editUserPage.form.role")}
|
||||
filteredBy="role"
|
||||
inputValue={searchInput}
|
||||
handleInputChange={handleSearchInput}
|
||||
value={
|
||||
form?.role
|
||||
?.filter((role) => role !== ROLES.SUPERADMIN)
|
||||
.map((role) => ({ role, _id: role })) || []
|
||||
}
|
||||
options={EDITABLE_ROLES}
|
||||
multiple={true}
|
||||
handleChange={handleRoleChange}
|
||||
/>
|
||||
<RoleTable
|
||||
roles={form?.role}
|
||||
handleDeleteRole={handleDeleteRole}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isLoading || isSaving}
|
||||
>
|
||||
{t("editUserPage.form.save")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUser;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useState } from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Proptypes from "prop-types";
|
||||
|
||||
const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
endIcon={<ArrowDropDownIcon sx={{ color: theme.palette.secondary.light }} />}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{t("teamPanel.addTeamMember.addMemberMenu")}
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
"& .MuiPaper-root": {
|
||||
minWidth: anchorEl?.offsetWidth || "auto",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
handleInviteOpen();
|
||||
}}
|
||||
>
|
||||
{t("teamPanel.inviteTeamMember")}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
handleIsRegisterOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("teamPanel.register")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AddMemberMenu.propTypes = {
|
||||
handleInviteOpen: Proptypes.func.isRequired,
|
||||
handleIsRegisterOpen: Proptypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddMemberMenu;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useState } from "react";
|
||||
import { newOrChangedCredentials } from "../../../../../Validation/validation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const useAddTeamMember = () => {
|
||||
const { t } = useTranslation();
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const clearErrors = () => setErrors({});
|
||||
|
||||
const validateFields = (name, value, formData) => {
|
||||
const { error } = newOrChangedCredentials.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false, context: { password: formData.password } }
|
||||
);
|
||||
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: error?.details?.[0]?.message || "",
|
||||
}));
|
||||
};
|
||||
|
||||
const validateForm = (formData, role) => {
|
||||
const { error } = newOrChangedCredentials.validate(formData, {
|
||||
abortEarly: false,
|
||||
context: { password: formData.password },
|
||||
});
|
||||
const formErrors = {};
|
||||
if (error) {
|
||||
for (const err of error.details) {
|
||||
formErrors[err.path[0]] = err.message;
|
||||
}
|
||||
}
|
||||
if (!role[0] || role.length === 0) {
|
||||
formErrors.role = t(
|
||||
"teamPanel.registerTeamMember.auth.common.inputs.role.errors.empty"
|
||||
);
|
||||
}
|
||||
if (Object.keys(formErrors).length > 0) {
|
||||
setErrors(formErrors);
|
||||
return false;
|
||||
}
|
||||
setErrors({});
|
||||
return true;
|
||||
};
|
||||
|
||||
return { errors, setErrors, clearErrors, validateFields, validateForm };
|
||||
};
|
||||
export default useAddTeamMember;
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Button, Stack } from "@mui/material";
|
||||
import { GenericDialog } from "../../../../Components/Dialog/genericDialog";
|
||||
import TextInput from "../../../../Components/Inputs/TextInput";
|
||||
import Select from "../../../../Components/Inputs/Select";
|
||||
import { useGetInviteToken } from "../../../../Hooks/inviteHooks";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useState } from "react";
|
||||
import PasswordTooltip from "../../../Auth/components/PasswordTooltip";
|
||||
import useAddTeamMember from "./hooks/useAddTeamMember";
|
||||
import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback";
|
||||
import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const INITIAL_FORM_STATE = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirm: "",
|
||||
teamId: "",
|
||||
};
|
||||
|
||||
const INITIAL_ROLE_STATE = ["user"];
|
||||
const AddTeamMember = ({ handleIsRegisterOpen, isRegisterOpen, onMemberAdded }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { errors, setErrors, clearErrors, validateFields, validateForm } =
|
||||
useAddTeamMember();
|
||||
const { feedback, handlePasswordFeedback } = usePasswordFeedback();
|
||||
const [getInviteToken, clearToken, isLoading, error, token, addTeamMember] =
|
||||
useGetInviteToken();
|
||||
const [form, setForm] = useState(INITIAL_FORM_STATE);
|
||||
const [role, setRole] = useState(INITIAL_ROLE_STATE);
|
||||
const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
|
||||
const closeAddMemberModal = () => {
|
||||
handleIsRegisterOpen(false);
|
||||
setForm(INITIAL_FORM_STATE);
|
||||
setRole(INITIAL_ROLE_STATE);
|
||||
clearErrors();
|
||||
clearToken();
|
||||
};
|
||||
|
||||
const onChange = (e) => {
|
||||
let { name, value } = e.target;
|
||||
if (name === "email") value = value.toLowerCase();
|
||||
const updatedForm = { ...form, [name]: value };
|
||||
validateFields(name, value, updatedForm);
|
||||
setForm(updatedForm);
|
||||
|
||||
if (name === "password" || name === "confirm") {
|
||||
handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors);
|
||||
}
|
||||
};
|
||||
|
||||
const onsubmitAddMember = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!validateForm(form, role)) return;
|
||||
try {
|
||||
setIsLoadingSubmit(true);
|
||||
await addTeamMember(form, role);
|
||||
createToast({
|
||||
body: t("teamPanel.registerToast.success"),
|
||||
});
|
||||
onMemberAdded();
|
||||
closeAddMemberModal();
|
||||
} catch (error) {
|
||||
const errorMsg = error.response?.data?.msg || error.message || "unknownError";
|
||||
createToast({
|
||||
type: "error",
|
||||
body: t(errorMsg),
|
||||
});
|
||||
} finally {
|
||||
setIsLoadingSubmit(false);
|
||||
}
|
||||
};
|
||||
const tErr = (key) => (key ? t([`teamPanel.registerTeamMember.${key}`, key]) : "");
|
||||
return (
|
||||
<>
|
||||
<GenericDialog
|
||||
title={t("teamPanel.addTeamMember.title")}
|
||||
description={t("teamPanel.addTeamMember.description")}
|
||||
open={isRegisterOpen}
|
||||
onClose={closeAddMemberModal}
|
||||
theme={theme}
|
||||
width={{ sm: "55%", md: "50%", lg: "40%", xl: "30%" }}
|
||||
>
|
||||
<TextInput
|
||||
name="firstName"
|
||||
label={t("auth.common.inputs.firstName.label")}
|
||||
isRequired={true}
|
||||
gap={theme.spacing(4)}
|
||||
placeholder={t("auth.common.inputs.firstName.placeholder")}
|
||||
value={form.firstName}
|
||||
onChange={onChange}
|
||||
error={errors.firstName ? true : false}
|
||||
helperText={errors.firstName ? tErr(errors.firstName) : null}
|
||||
sx={{ mb: errors.firstName ? theme.spacing(15) : theme.spacing(5) }}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
name="lastName"
|
||||
label={t("auth.common.inputs.lastName.label")}
|
||||
isRequired={true}
|
||||
gap={theme.spacing(4)}
|
||||
placeholder={t("auth.common.inputs.lastName.placeholder")}
|
||||
value={form.lastName}
|
||||
onChange={onChange}
|
||||
error={errors.lastName ? true : false}
|
||||
helperText={errors.lastName ? tErr(errors.lastName) : null}
|
||||
sx={{ mb: errors.lastName ? theme.spacing(15) : theme.spacing(5) }}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
type="email"
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
name="email"
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
id="input-team-member"
|
||||
placeholder={t("teamPanel.email")}
|
||||
value={form.email}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email ? tErr(errors.email) : null}
|
||||
sx={{ mb: errors.email ? theme.spacing(15) : theme.spacing(5) }}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label={t("teamPanel.role")}
|
||||
id="team-member-role"
|
||||
name="role"
|
||||
required={true}
|
||||
placeholder={t("teamPanel.selectRole")}
|
||||
isHidden={true}
|
||||
value={role[0] || ""}
|
||||
sx={{ mb: theme.spacing(5) }}
|
||||
onChange={(event) => setRole([event.target.value])}
|
||||
items={[
|
||||
{ _id: "admin", name: t("roles.admin") },
|
||||
{ _id: "user", name: t("roles.teamMember") },
|
||||
]}
|
||||
/>
|
||||
|
||||
<PasswordTooltip
|
||||
feedback={feedback}
|
||||
form={form}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password && errors.password[0] ? true : false}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
sx={{ mb: theme.spacing(5) }}
|
||||
/>
|
||||
</PasswordTooltip>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
name="confirm"
|
||||
label={t("auth.common.inputs.passwordConfirm.label")}
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onChange}
|
||||
error={errors.confirm && errors.confirm[0] ? true : false}
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
sx={{ mb: theme.spacing(5) }}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(10)}
|
||||
mt={theme.spacing(8)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={closeAddMemberModal}
|
||||
disabled={isLoadingSubmit}
|
||||
>
|
||||
{t("teamPanel.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={onsubmitAddMember}
|
||||
disabled={isLoadingSubmit}
|
||||
>
|
||||
{t("teamPanel.addTeamMember.addButton")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</GenericDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
AddTeamMember.propTypes = {
|
||||
handleIsRegisterOpen: PropTypes.func.isRequired,
|
||||
isRegisterOpen: PropTypes.bool.isRequired,
|
||||
onMemberAdded: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddTeamMember;
|
||||
+3
-3
@@ -2,10 +2,10 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import { useState } from "react";
|
||||
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 { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import Alert from "../../Alert";
|
||||
import Alert from "../../../Components/Alert";
|
||||
import { update } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
+5
-6
@@ -2,16 +2,16 @@ import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
|
||||
import Avatar from "../../Avatar";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import ImageUpload from "../../Inputs/ImageUpload";
|
||||
import Avatar from "../../../Components/Avatar";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import ImageUpload from "../../../Components/Inputs/ImageUpload";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
import Dialog from "../../Dialog";
|
||||
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
@@ -191,7 +191,6 @@ const ProfilePanel = () => {
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
className="edit-profile-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={SPACING_GAP}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DataTable from "../../../../Components/Table";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ROLES } from "../../../../Utils/roleUtils";
|
||||
|
||||
const RoleTable = ({ roles, handleDeleteRole }) => {
|
||||
const { t } = useTranslation();
|
||||
const HEADERS = [
|
||||
{
|
||||
id: "name",
|
||||
content: <Typography>{t("editUserPage.table.roleHeader")}</Typography>,
|
||||
render: (row) => {
|
||||
return row;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
content: <Typography>{t("editUserPage.table.actionHeader")}</Typography>,
|
||||
render: (row) => {
|
||||
if (row === ROLES.SUPERADMIN) return null;
|
||||
return (
|
||||
<DeleteOutlineRoundedIcon
|
||||
onClick={() => {
|
||||
handleDeleteRole(row);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<DataTable
|
||||
headers={HEADERS}
|
||||
data={roles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleTable;
|
||||
+40
-18
@@ -3,14 +3,18 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Select from "../../Inputs/Select";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
import DataTable from "../../Table/";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
|
||||
import AddTeamMember from "../components/AddTeamMember";
|
||||
import DataTable from "../../../Components/Table";
|
||||
import { useGetInviteToken } from "../../../Hooks/inviteHooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import AddMemberMenu from "./AddMemberMenu";
|
||||
/**
|
||||
* TeamPanel component manages the organization and team members,
|
||||
* providing functionalities like renaming the organization, managing team members,
|
||||
@@ -34,8 +38,9 @@ const TeamPanel = () => {
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSendingInvite, setIsSendingInvite] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken();
|
||||
const isSuperAdmin = useIsSuperAdmin();
|
||||
|
||||
const headers = [
|
||||
{
|
||||
@@ -62,7 +67,10 @@ const TeamPanel = () => {
|
||||
render: (row) => row.role,
|
||||
},
|
||||
];
|
||||
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(false);
|
||||
const refreshTeamList = () => {
|
||||
setRefreshTrigger((prev) => !prev);
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchTeam = async () => {
|
||||
try {
|
||||
@@ -76,7 +84,7 @@ const TeamPanel = () => {
|
||||
};
|
||||
|
||||
fetchTeam();
|
||||
}, []);
|
||||
}, [refreshTrigger]);
|
||||
|
||||
useEffect(() => {
|
||||
const ROLE_MAP = {
|
||||
@@ -106,6 +114,10 @@ const TeamPanel = () => {
|
||||
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
|
||||
}, [errors, toInvite.email]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isRegisterOpen, setIsRegisterOpen] = useState(false);
|
||||
const handleIsRegisterOpen = (open) => {
|
||||
setIsRegisterOpen(open);
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
@@ -135,7 +147,6 @@ const TeamPanel = () => {
|
||||
const handleGetToken = async () => {
|
||||
await getInviteToken({ email: toInvite.email, role: toInvite.role });
|
||||
};
|
||||
|
||||
const handleInviteMember = async () => {
|
||||
if (!toInvite.email) {
|
||||
setErrors((prev) => ({ ...prev, email: "Email is required." }));
|
||||
@@ -184,7 +195,6 @@ const TeamPanel = () => {
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
className="team-panel table-container"
|
||||
value="team"
|
||||
sx={{
|
||||
"& h1": {
|
||||
@@ -237,22 +247,34 @@ const TeamPanel = () => {
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
{t("teamPanel.inviteTeamMember")}
|
||||
</Button>
|
||||
|
||||
<AddTeamMember
|
||||
handleIsRegisterOpen={handleIsRegisterOpen}
|
||||
isRegisterOpen={isRegisterOpen}
|
||||
onMemberAdded={refreshTeamList}
|
||||
/>
|
||||
<AddMemberMenu
|
||||
handleInviteOpen={() => setIsOpen(true)}
|
||||
handleIsRegisterOpen={handleIsRegisterOpen}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={data}
|
||||
config={{ emptyView: t("teamPanel.noMembers") }}
|
||||
config={{
|
||||
emptyView: t("teamPanel.noMembers"),
|
||||
rowSX: {
|
||||
cursor: isSuperAdmin ? "pointer" : "default",
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
if (isSuperAdmin) {
|
||||
navigate(`/account/team/${row.id}`);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<GenericDialog
|
||||
title={t("teamPanel.inviteNewTeamMember")}
|
||||
description={t("teamPanel.inviteDescription")}
|
||||
@@ -1,38 +0,0 @@
|
||||
.account h1.MuiTypography-root,
|
||||
.account p.MuiTypography-root,
|
||||
.account input,
|
||||
.account button,
|
||||
.account td,
|
||||
.account .MuiSelect-select {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.account h1.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.account .MuiTabPanel-root {
|
||||
padding: 0;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.account button:not(.MuiIconButton-root) {
|
||||
min-height: 34px;
|
||||
}
|
||||
.account .field {
|
||||
flex: 1;
|
||||
}
|
||||
#modal-delete-account,
|
||||
#modal-edit-org-name,
|
||||
#modal-invite-member {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
|
||||
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
}
|
||||
.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.account .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -5,11 +5,10 @@ import { useSelector } from "react-redux";
|
||||
import { Box, Tab, useTheme } from "@mui/material";
|
||||
import CustomTabList from "../../Components/Tab";
|
||||
import TabContext from "@mui/lab/TabContext";
|
||||
import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
|
||||
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
|
||||
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
|
||||
import ProfilePanel from "./components/ProfilePanel";
|
||||
import PasswordPanel from "./components/PasswordPanel";
|
||||
import TeamPanel from "./components/TeamPanel";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Account component renders a settings page with tabs for Profile, Password, and Team settings.
|
||||
@@ -33,7 +32,9 @@ const Account = ({ open = "profile" }) => {
|
||||
{ name: t("menu.password"), value: "password" },
|
||||
{ name: t("menu.team"), value: "team" },
|
||||
];
|
||||
|
||||
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
|
||||
|
||||
if (hideTeams) {
|
||||
tabList = [
|
||||
{ name: t("menu.profile"), value: "profile" },
|
||||
@@ -63,7 +64,6 @@ const Account = ({ open = "profile" }) => {
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="account"
|
||||
px={theme.spacing(20)}
|
||||
py={theme.spacing(12)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Hook to avoid double submits and manage loading state
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
const useLoadingSubmit = () => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const executeSubmit = useCallback(
|
||||
async (submitFunction) => {
|
||||
if (submitting) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
return await submitFunction();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[submitting]
|
||||
);
|
||||
|
||||
return { submitting, executeSubmit };
|
||||
};
|
||||
|
||||
export default useLoadingSubmit;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const useLoginForm = () => {
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const onChange = (e) => {
|
||||
let { name, value } = e.target;
|
||||
if (name === "email") {
|
||||
value = value.toLowerCase();
|
||||
}
|
||||
const updatedForm = { ...form, [name]: value };
|
||||
setForm(updatedForm);
|
||||
};
|
||||
return [form, onChange];
|
||||
};
|
||||
|
||||
export default useLoginForm;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { login } from "../../../../Features/Auth/authSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const useLoginSubmit = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLoginSubmit = async (form, setErrors) => {
|
||||
const action = await dispatch(login(form));
|
||||
if (action.payload.success) {
|
||||
navigate("/uptime");
|
||||
createToast({
|
||||
body: t("auth.login.toasts.success"),
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
if (action.payload.msg === "Incorrect password")
|
||||
setErrors({
|
||||
password: t("auth.login.errors.password.incorrect"),
|
||||
});
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: t("auth.login.toasts.incorrectPassword"),
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: t("common.toasts.unknownError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return [handleLoginSubmit];
|
||||
};
|
||||
|
||||
export default useLoginSubmit;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState } from "react";
|
||||
import { loginCredentials } from "../../../../Validation/validation";
|
||||
|
||||
const useValidateLoginForm = () => {
|
||||
const [errors, setErrors] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const validateField = (name, value) => {
|
||||
const { error } = loginCredentials.validate({ [name]: value });
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: error?.details?.[0]?.message || "",
|
||||
}));
|
||||
};
|
||||
const validateForm = (form) => {
|
||||
const { error } = loginCredentials.validate(form, { abortEarly: false });
|
||||
if (error) {
|
||||
const formErrors = {};
|
||||
for (const err of error.details) {
|
||||
formErrors[err.path[0]] = err.message;
|
||||
}
|
||||
setErrors(formErrors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return [errors, setErrors, validateField, validateForm];
|
||||
};
|
||||
|
||||
export default useValidateLoginForm;
|
||||
@@ -1,167 +1,102 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import AuthHeader from "../components/AuthHeader";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import { loginCredentials } from "../../../Validation/validation";
|
||||
import TextLink from "../../../Components/TextLink";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import AuthPageWrapper from "../components/AuthPageWrapper";
|
||||
// Utils
|
||||
import { login } from "../../../Features/Auth/authSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import useLoginForm from "./hooks/useLoginForm";
|
||||
import useValidateLoginForm from "./hooks/useValidateLoginForm";
|
||||
import useLoginSubmit from "./hooks/useLoginSubmit";
|
||||
import useLoadingSubmit from "./hooks/useLoadingSubmit";
|
||||
|
||||
const Login = () => {
|
||||
// Local state
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [form, onChange] = useLoginForm();
|
||||
const [errors, setErrors, validateField, validateForm] = useValidateLoginForm();
|
||||
const [handleLoginSubmit] = useLoginSubmit();
|
||||
const { submitting, executeSubmit } = useLoadingSubmit();
|
||||
|
||||
// Handlers
|
||||
const onChange = (e) => {
|
||||
let { name, value } = e.target;
|
||||
if (name === "email") {
|
||||
value = value.toLowerCase();
|
||||
}
|
||||
const updatedForm = { ...form, [name]: value };
|
||||
const { error } = loginCredentials.validate({ [name]: value });
|
||||
setForm(updatedForm);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: error?.details?.[0]?.message || "",
|
||||
}));
|
||||
const handleChange = (e) => {
|
||||
onChange(e);
|
||||
validateField(e.target.name, e.target.value);
|
||||
};
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const toSubmit = { ...form };
|
||||
const { error } = loginCredentials.validate(toSubmit, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
const formErrors = {};
|
||||
for (const err of error.details) {
|
||||
formErrors[err.path[0]] = err.message;
|
||||
}
|
||||
setErrors(formErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
const action = await dispatch(login(form));
|
||||
if (action.payload.success) {
|
||||
navigate("/uptime");
|
||||
createToast({
|
||||
body: t("auth.login.toasts.success"),
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
if (action.payload.msg === "Incorrect password")
|
||||
setErrors({
|
||||
password: t("auth.login.errors.password.incorrect"),
|
||||
});
|
||||
// dispatch errors
|
||||
createToast({
|
||||
body: t("auth.login.toasts.incorrectPassword"),
|
||||
});
|
||||
} else {
|
||||
// unknown errors
|
||||
createToast({
|
||||
body: t("common.toasts.unknownError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
const isValid = validateForm(form);
|
||||
if (!isValid) return;
|
||||
await executeSubmit(() => handleLoginSubmit(form, setErrors));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
minHeight="100vh"
|
||||
<AuthPageWrapper
|
||||
welcome={t("auth.login.welcome")}
|
||||
heading={t("auth.login.heading")}
|
||||
>
|
||||
<AuthHeader />
|
||||
<Stack
|
||||
margin="auto"
|
||||
component="form"
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(10)}
|
||||
padding={theme.spacing(8)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={onSubmit}
|
||||
sx={{
|
||||
width: {
|
||||
sm: "80%",
|
||||
md: "70%",
|
||||
lg: "65%",
|
||||
xl: "65%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h1">{t("auth.login.heading")}</Typography>
|
||||
|
||||
<Stack
|
||||
component="form"
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
alignSelf="center"
|
||||
justifyContent="center"
|
||||
borderRadius={theme.spacing(5)}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
padding={theme.spacing(12)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={onSubmit}
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password ? true : false}
|
||||
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
|
||||
loading={submitting}
|
||||
>
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onChange}
|
||||
error={errors.password ? true : false}
|
||||
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
|
||||
endAdornment={<PasswordEndAdornment />}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "30%", alignSelf: "flex-end" }}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
<TextLink
|
||||
text={t("auth.login.links.forgotPassword")}
|
||||
linkText={t("auth.login.links.forgotPasswordLink")}
|
||||
href="/forgot-password"
|
||||
/>
|
||||
<TextLink
|
||||
text={t("auth.login.links.register")}
|
||||
linkText={t("auth.login.links.registerLink")}
|
||||
href="/register"
|
||||
/>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<TextLink
|
||||
text={t("auth.login.links.forgotPassword")}
|
||||
linkText={t("auth.login.links.forgotPasswordLink")}
|
||||
href="/forgot-password"
|
||||
/>
|
||||
<TextLink
|
||||
text={t("auth.login.links.register")}
|
||||
linkText={t("auth.login.links.registerLink")}
|
||||
href="/register"
|
||||
/>
|
||||
</AuthPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,10 +1,10 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import AuthHeader from "../components/AuthHeader";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Check from "../../../Components/Check/Check";
|
||||
import Button from "@mui/material/Button";
|
||||
import Box from "@mui/material/Box";
|
||||
import PasswordTooltip from "../components/PasswordTooltip";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -16,6 +16,7 @@ import { useParams } from "react-router-dom";
|
||||
import { networkService } from "../../../main";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { register } from "../../../Features/Auth/authSlice";
|
||||
import AuthPageWrapper from "../components/AuthPageWrapper";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
@@ -195,59 +196,38 @@ const Register = ({ superAdminExists }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
minHeight="100vh"
|
||||
<AuthPageWrapper
|
||||
heading={t("auth.registration.heading.user")}
|
||||
welcome={t("auth.registration.welcome")}
|
||||
>
|
||||
<AuthHeader />
|
||||
<Stack
|
||||
margin="auto"
|
||||
component="form"
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(10)}
|
||||
padding={theme.spacing(8)}
|
||||
gap={theme.spacing(8)}
|
||||
onSubmit={onSubmit}
|
||||
sx={{
|
||||
width: {
|
||||
sm: "80%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="h1">{t("auth.registration.heading.user")}</Typography>
|
||||
|
||||
<Typography variant="h2">
|
||||
{superAdminExists
|
||||
? t("auth.registration.description.user")
|
||||
: t("auth.registration.description.superAdmin")}
|
||||
</Typography>
|
||||
<Stack
|
||||
component="form"
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
alignSelf="center"
|
||||
justifyContent="center"
|
||||
border={1}
|
||||
borderRadius={theme.spacing(5)}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
padding={theme.spacing(12)}
|
||||
gap={theme.spacing(12)}
|
||||
onSubmit={onSubmit}
|
||||
direction={{ xs: "column", lg: "row" }}
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Typography variant="h1">
|
||||
{superAdminExists
|
||||
? t("auth.registration.heading.user")
|
||||
: t("auth.registration.heading.superAdmin")}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{superAdminExists
|
||||
? t("auth.registration.description.user")
|
||||
: t("auth.registration.description.superAdmin")}
|
||||
</Typography>
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<TextInput
|
||||
name="firstName"
|
||||
sx={{ flex: 1 }}
|
||||
label={t("auth.common.inputs.firstName.label")}
|
||||
width="100%"
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.firstName.placeholder")}
|
||||
autoComplete="given-name"
|
||||
@@ -259,6 +239,9 @@ const Register = ({ superAdminExists }) => {
|
||||
<TextInput
|
||||
name="lastName"
|
||||
label={t("auth.common.inputs.lastName.label")}
|
||||
sx={{ flex: 1 }}
|
||||
width="100%"
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.lastName.placeholder")}
|
||||
autoComplete="family-name"
|
||||
@@ -267,82 +250,76 @@ const Register = ({ superAdminExists }) => {
|
||||
error={errors.lastName ? true : false}
|
||||
helperText={errors.lastName ? t(errors.lastName) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onPasswordChange}
|
||||
error={errors.password && errors.password[0] ? true : false}
|
||||
helperText={
|
||||
errors.password === "auth.common.inputs.password.errors.empty"
|
||||
? t(errors.password)
|
||||
: ""
|
||||
} // Other errors are related to required password conditions and are visualized below the input
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
name="confirm"
|
||||
label={t("auth.common.inputs.passwordConfirm.label")}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onPasswordChange}
|
||||
error={errors.confirm && errors.confirm[0] ? true : false}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.length.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.length.highlighted")}
|
||||
variant={feedback.length}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.special.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.special.highlighted")}
|
||||
variant={feedback.special}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.number.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.number.highlighted")}
|
||||
variant={feedback.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.uppercase.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.uppercase.highlighted")}
|
||||
variant={feedback.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.lowercase.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.lowercase.highlighted")}
|
||||
variant={feedback.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={t("auth.common.inputs.password.rules.match.beginning")}
|
||||
text={t("auth.common.inputs.password.rules.match.highlighted")}
|
||||
variant={feedback.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{ width: "30%", alignSelf: "flex-end" }}
|
||||
>
|
||||
{t("auth.common.navigation.continue")}
|
||||
</Button>
|
||||
</Stack>
|
||||
<TextInput
|
||||
type="email"
|
||||
name="email"
|
||||
gap={theme.spacing(4)}
|
||||
label={t("auth.common.inputs.email.label")}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.email.placeholder")}
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
|
||||
/>
|
||||
<PasswordTooltip
|
||||
feedback={feedback}
|
||||
form={form}
|
||||
>
|
||||
<Box>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label={t("auth.common.inputs.password.label")}
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder="••••••••••"
|
||||
autoComplete="current-password"
|
||||
value={form.password}
|
||||
onChange={onPasswordChange}
|
||||
error={errors.password && errors.password[0] ? true : false}
|
||||
helperText={
|
||||
errors.password === "auth.common.inputs.password.errors.empty"
|
||||
? t(errors.password)
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</PasswordTooltip>
|
||||
<TextInput
|
||||
type="password"
|
||||
id="register-confirm-input"
|
||||
name="confirm"
|
||||
label={t("auth.common.inputs.passwordConfirm.label")}
|
||||
gap={theme.spacing(4)}
|
||||
isRequired={true}
|
||||
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
|
||||
autoComplete="current-password"
|
||||
value={form.confirm}
|
||||
onChange={onPasswordChange}
|
||||
marginBottom={theme.spacing(4)}
|
||||
error={errors.confirm && errors.confirm[0] ? true : false}
|
||||
/>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
type="submit"
|
||||
sx={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
fontWeight: 700,
|
||||
mt: theme.spacing(10),
|
||||
}}
|
||||
>
|
||||
{t("auth.common.navigation.continue")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</AuthPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import ThemeSwitch from "../../../Components/ThemeSwitch";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const AuthHeader = () => {
|
||||
const AuthHeader = ({ hideLogo = false }) => {
|
||||
// Hooks
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -27,8 +27,12 @@ const AuthHeader = () => {
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
|
||||
{!hideLogo && (
|
||||
<>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import Background from "../../../assets/Images/background-grid.svg?react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import AuthHeader from "../components/AuthHeader";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const AuthPageWrapper = ({ children, heading, welcome }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(10)}
|
||||
minHeight="100vh"
|
||||
position="relative"
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{ overflow: "hidden" }}
|
||||
>
|
||||
<AuthHeader hideLogo={true} />
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "0%",
|
||||
transform: "translate(-40%, -40%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
transform: "translate(45%, 55%)",
|
||||
zIndex: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{
|
||||
borderRadius: theme.spacing(8),
|
||||
boxShadow: theme.palette.tertiary.cardShadow,
|
||||
margin: "auto",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(10),
|
||||
padding: theme.spacing(20),
|
||||
zIndex: 1,
|
||||
position: "relative",
|
||||
width: {
|
||||
sm: "60%",
|
||||
md: "50%",
|
||||
lg: "40%",
|
||||
xl: "30%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
mb={theme.spacing(10)}
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 60, sm: 70, md: 80 },
|
||||
}}
|
||||
/>
|
||||
<Logo style={{ width: "100%", height: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
mb={theme.spacing(4)}
|
||||
textAlign="center"
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
mb={theme.spacing(2)}
|
||||
>
|
||||
{welcome}
|
||||
</Typography>
|
||||
<Typography variant="h1">{heading}</Typography>
|
||||
</Stack>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthPageWrapper;
|
||||
|
||||
AuthPageWrapper.propTypes = {
|
||||
children: PropTypes.node,
|
||||
heading: PropTypes.node,
|
||||
welcome: PropTypes.node,
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import Check from "../../../Components/Check/Check";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Tooltip, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const PasswordTooltip = ({ feedback, form, children }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const hasPassword = form.password.length > 0;
|
||||
const hasInvalidFeedback = Object.values(feedback).some(
|
||||
(status) => status !== "success"
|
||||
);
|
||||
const showPasswordTooltip = hasPassword && hasInvalidFeedback;
|
||||
return (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
arrow
|
||||
open={showPasswordTooltip}
|
||||
title={
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
|
||||
>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.length.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.length.highlighted")
|
||||
}
|
||||
variant={feedback.length}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.special.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.special.highlighted")
|
||||
}
|
||||
variant={feedback.special}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.number.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.number.highlighted")
|
||||
}
|
||||
variant={feedback.number}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.uppercase.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.uppercase.highlighted")
|
||||
}
|
||||
variant={feedback.uppercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.lowercase.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.lowercase.highlighted")
|
||||
}
|
||||
variant={feedback.lowercase}
|
||||
/>
|
||||
<Check
|
||||
noHighlightText={
|
||||
t("auth.common.inputs.password.rules.match.beginning") +
|
||||
" " +
|
||||
t("auth.common.inputs.password.rules.match.highlighted")
|
||||
}
|
||||
variant={feedback.confirm}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
backgroundColor: theme.palette.tertiary.background,
|
||||
border: `0.5px solid ${theme.palette.primary.lowContrast}90`,
|
||||
borderRadius: theme.spacing(4),
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: "auto",
|
||||
maxWidth: { xs: "25vw", md: "none" },
|
||||
whiteSpace: { xs: "normal", md: "nowrap" },
|
||||
paddingTop: theme.spacing(8),
|
||||
px: theme.spacing(8),
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: theme.palette.tertiary.background,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span style={{ display: "inline-block", width: "100%" }}>{children}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
PasswordTooltip.propTypes = {
|
||||
feedback: PropTypes.shape({
|
||||
length: PropTypes.string,
|
||||
special: PropTypes.string,
|
||||
number: PropTypes.string,
|
||||
uppercase: PropTypes.string,
|
||||
lowercase: PropTypes.string,
|
||||
confirm: PropTypes.string,
|
||||
}),
|
||||
form: PropTypes.shape({
|
||||
password: PropTypes.string.isRequired,
|
||||
}),
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default PasswordTooltip;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState } from "react";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
|
||||
const usePasswordFeedback = () => {
|
||||
const [feedback, setFeedback] = useState({});
|
||||
const getFeedbackStatus = (form, errors, field, criteria) => {
|
||||
const fieldErrors = errors?.[field];
|
||||
const isFieldEmpty = form?.[field]?.length === 0;
|
||||
const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
|
||||
const isCorrect = !isFieldEmpty && !hasError;
|
||||
|
||||
if (isCorrect) {
|
||||
return "success";
|
||||
} else if (hasError) {
|
||||
return "error";
|
||||
} else {
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordFeedback = (updatedForm, name, value, form, errors, setErrors) => {
|
||||
const validateValue = { [name]: value };
|
||||
const validateOptions = { abortEarly: false, context: { password: form.password } };
|
||||
if (name === "password" && form.confirm.length > 0) {
|
||||
validateValue.confirm = form.confirm;
|
||||
validateOptions.context = { password: value };
|
||||
} else if (name === "confirm") {
|
||||
validateValue.password = form.password;
|
||||
}
|
||||
const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
|
||||
|
||||
const pwdErrors = error?.details.map((error) => ({
|
||||
path: error.path[0],
|
||||
type: error.type,
|
||||
}));
|
||||
|
||||
const errorsByPath =
|
||||
pwdErrors &&
|
||||
pwdErrors.reduce((acc, { path, type }) => {
|
||||
if (!acc[path]) {
|
||||
acc[path] = [];
|
||||
}
|
||||
acc[path].push(type);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const oldErrors = { ...errors };
|
||||
if (name === "password") {
|
||||
oldErrors.password = undefined;
|
||||
} else if (name === "confirm") {
|
||||
oldErrors.confirm = undefined;
|
||||
}
|
||||
const newErrors = { ...oldErrors, ...errorsByPath };
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
const newFeedback = {
|
||||
length: getFeedbackStatus(updatedForm, errorsByPath, "password", "string.min"),
|
||||
special: getFeedbackStatus(updatedForm, errorsByPath, "password", "special"),
|
||||
number: getFeedbackStatus(updatedForm, errorsByPath, "password", "number"),
|
||||
uppercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "uppercase"),
|
||||
lowercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "lowercase"),
|
||||
confirm: getFeedbackStatus(updatedForm, errorsByPath, "confirm", "different"),
|
||||
};
|
||||
|
||||
setFeedback(newFeedback);
|
||||
};
|
||||
return { feedback, handlePasswordFeedback, getFeedbackStatus };
|
||||
};
|
||||
|
||||
export default usePasswordFeedback;
|
||||
@@ -1,5 +1,10 @@
|
||||
//Components
|
||||
import Table from "../../../../Components/Table";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import DataTable from "../../../../Components/Table";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableSkeleton from "../../../../Components/Table/skeleton";
|
||||
import Pagination from "../../../../Components/Table/TablePagination";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
@@ -16,7 +21,68 @@ import { useTranslation } from "react-i18next";
|
||||
import { useFetchChecksTeam } from "../../../../Hooks/checkHooks";
|
||||
import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks";
|
||||
import { useResolveIncident } from "../../../../Hooks/checkHooks";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { Button, Typography, useTheme } from "@mui/material";
|
||||
import { lighten } from "@mui/material/styles";
|
||||
|
||||
const GetTooltip = (row) => {
|
||||
const theme = useTheme();
|
||||
const phases = row?.timings?.phases;
|
||||
|
||||
const phaseKeyFormattingMap = {
|
||||
firstByte: "first byte",
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
backgroundColor={lighten(theme.palette.primary.main, 0.1)}
|
||||
border={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
py={theme.spacing(2)}
|
||||
px={theme.spacing(4)}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Status code: ${row?.statusCode}`}</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Response time: ${row?.responseTime} ms`}</Typography>
|
||||
{phases && (
|
||||
<>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>{`Request timing: `}</Typography>
|
||||
<Table
|
||||
size="small"
|
||||
sx={{ ml: theme.spacing(2), mt: theme.spacing(2) }}
|
||||
>
|
||||
<TableBody>
|
||||
{Object.keys(phases)?.map((phaseKey) => (
|
||||
<TableRow key={phaseKey}>
|
||||
<TableCell sx={{ border: "none", p: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="success"
|
||||
>
|
||||
{`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ border: "none", p: 0 }}>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
variant="body2"
|
||||
>{`${phases[phaseKey]} ms`}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const IncidentTable = ({
|
||||
isLoading,
|
||||
@@ -167,9 +233,10 @@ const IncidentTable = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={checks}
|
||||
config={{ tooltipContent: GetTooltip }}
|
||||
/>
|
||||
<Pagination
|
||||
paginationLabel={t("incidentsTablePaginationLabel")}
|
||||
|
||||
@@ -2,18 +2,22 @@ import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Background from "../../../../assets/Images/background-grid.svg?react";
|
||||
import MonitorHeartOutlinedIcon from "@mui/icons-material/MonitorHeartOutlined";
|
||||
import TaskAltOutlinedIcon from "@mui/icons-material/TaskAltOutlined";
|
||||
import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined";
|
||||
import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded";
|
||||
import AlertIcon from "../../../../assets/icons/alert-icon.svg?react";
|
||||
import CheckIcon from "../../../../assets/icons/check-icon.svg?react";
|
||||
import CloseIcon from "../../../../assets/icons/close-icon.svg?react";
|
||||
import WarningIcon from "../../../../assets/icons/warning-icon.svg?react";
|
||||
|
||||
const StatusBox = ({ title, value, status }) => {
|
||||
const theme = useTheme();
|
||||
let sharedStyles = {
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
opacity: 0.5,
|
||||
"& svg path": { stroke: theme.palette.primary.contrastTextTertiary },
|
||||
"& svg": {
|
||||
width: 20,
|
||||
height: 20,
|
||||
opacity: 0.9,
|
||||
"& path": { stroke: theme.palette.primary.contrastTextTertiary, strokeWidth: 1.7 },
|
||||
},
|
||||
};
|
||||
|
||||
let color;
|
||||
@@ -22,28 +26,28 @@ const StatusBox = ({ title, value, status }) => {
|
||||
color = theme.palette.success.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<TaskAltOutlinedIcon fontSize="small" />
|
||||
<CheckIcon />
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "down") {
|
||||
color = theme.palette.error.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<CancelOutlinedIcon fontSize="small" />
|
||||
<CloseIcon />
|
||||
</Box>
|
||||
);
|
||||
} else if (status === "paused") {
|
||||
color = theme.palette.warning.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<WarningAmberRoundedIcon fontSize="small" />
|
||||
<WarningIcon />
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
color = theme.palette.accent.main;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
|
||||
<MonitorHeartOutlinedIcon fontSize="small" />
|
||||
<AlertIcon />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Box, Button } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks";
|
||||
import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks";
|
||||
import { useAckAllChecks } from "../../Hooks/checkHooks";
|
||||
import { useAcknowledgeChecks } from "../../Hooks/checkHooks";
|
||||
import { useState, useEffect } from "react";
|
||||
import NetworkError from "../../Components/GenericFallback/NetworkError";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -34,7 +34,7 @@ const Incidents = () => {
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
//Hooks
|
||||
const [ackAllChecks, ackAllLoading] = useAckAllChecks();
|
||||
const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks();
|
||||
|
||||
//Utils
|
||||
const theme = useTheme();
|
||||
@@ -62,8 +62,9 @@ const Incidents = () => {
|
||||
setMonitorLookup(monitorLookup);
|
||||
}, [monitors]);
|
||||
|
||||
const handleAckAllChecks = () => {
|
||||
ackAllChecks(setUpdateTrigger);
|
||||
const handleAcknowledge = () => {
|
||||
const monitorId = selectedMonitor === "0" ? null : selectedMonitor;
|
||||
acknowledge(setUpdateTrigger, monitorId);
|
||||
};
|
||||
|
||||
if (networkError || networkErrorSummary) {
|
||||
@@ -81,10 +82,12 @@ const Incidents = () => {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleAckAllChecks}
|
||||
disabled={ackAllLoading}
|
||||
onClick={handleAcknowledge}
|
||||
disabled={isLoadingAcknowledge}
|
||||
>
|
||||
{t("incidentsPageActionResolve")}
|
||||
{selectedMonitor === "0"
|
||||
? t("incidentsPageActionResolveAll")
|
||||
: t("incidentsPageActionResolveMonitor")}
|
||||
</Button>
|
||||
</Box>
|
||||
<StatusBoxes
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import ConfigBox from "../../../../Components/ConfigBox";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { CustomThreshold } from "../Components/CustomThreshold";
|
||||
import { capitalizeFirstLetter } from "../../../../Utils/stringUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
const CustomAlertsSection = ({
|
||||
errors,
|
||||
onChange,
|
||||
infrastructureMonitor,
|
||||
handleCheckboxChange,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const METRICS = ["cpu", "memory", "disk", "temperature"];
|
||||
const METRIC_PREFIX = "usage_";
|
||||
const hasAlertError = (errors) => {
|
||||
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
|
||||
};
|
||||
const getAlertError = (errors) => {
|
||||
const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX));
|
||||
return errorKey ? errors[errorKey] : null;
|
||||
};
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("infrastructureCustomizeAlerts")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("infrastructureAlertNotificationDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
{METRICS.map((metric) => {
|
||||
return (
|
||||
<CustomThreshold
|
||||
key={metric}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
errors={errors}
|
||||
checkboxId={metric}
|
||||
checkboxName={metric}
|
||||
checkboxLabel={
|
||||
metric !== "cpu" ? capitalizeFirstLetter(metric) : metric.toUpperCase()
|
||||
}
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
isChecked={infrastructureMonitor[metric]}
|
||||
fieldId={METRIC_PREFIX + metric}
|
||||
fieldName={METRIC_PREFIX + metric}
|
||||
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
|
||||
onFieldChange={onChange}
|
||||
alertUnit={metric == "temperature" ? "°C" : "%"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{hasAlertError(errors) && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{getAlertError(errors)}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
CustomAlertsSection.propTypes = {
|
||||
errors: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
infrastructureMonitor: PropTypes.object.isRequired,
|
||||
handleCheckboxChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CustomAlertsSection;
|
||||
@@ -62,14 +62,15 @@ export const CustomThreshold = ({
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
width: "50%",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
direction={{ sm: "column", md: "row" }}
|
||||
spacing={theme.spacing(2)}
|
||||
>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: { md: "45%", lg: "25%", xl: "20%" },
|
||||
}}
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
name={checkboxName}
|
||||
@@ -81,8 +82,10 @@ export const CustomThreshold = ({
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
justifyContent: "flex-end",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
alignItems="center"
|
||||
spacing={theme.spacing(4)}
|
||||
>
|
||||
<TextInput
|
||||
maxWidth="var(--env-var-width-4)"
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import Dialog from "../../../../Components/Dialog";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
alignSelf="flex-end"
|
||||
ml="auto"
|
||||
>
|
||||
<Button
|
||||
onClick={handlePause}
|
||||
loading={isBusy}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
{t("pause")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
{t("resume")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isBusy}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{ ml: theme.spacing(6) }}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
theme={theme}
|
||||
title={t("deleteDialogTitle")}
|
||||
description={t("deleteDialogDescription")}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
confirmationButtonLabel={t("delete")}
|
||||
onConfirm={handleRemove}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorActionButtons.propTypes = {
|
||||
monitor: PropTypes.object.isRequired,
|
||||
isBusy: PropTypes.bool.isRequired,
|
||||
handlePause: PropTypes.func.isRequired,
|
||||
handleRemove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MonitorActionButtons;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PulseDot from "../../../../Components/Animated/PulseDot";
|
||||
import PropTypes from "prop-types";
|
||||
const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: { offset: [0, -8] },
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="monitorUrl"
|
||||
>
|
||||
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: theme.spacing(2),
|
||||
height: theme.spacing(2),
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.primary.contrastTextTertiary,
|
||||
opacity: 0.8,
|
||||
left: theme.spacing(-5),
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("editing")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorStatusHeader.propTypes = {
|
||||
monitor: PropTypes.object.isRequired,
|
||||
infrastructureMonitor: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default MonitorStatusHeader;
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState, useCallback } from "react";
|
||||
const useInfrastructureMonitorForm = () => {
|
||||
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
notify_email: false,
|
||||
interval: 0.25,
|
||||
statusWindowSize: 5,
|
||||
statusWindowThreshold: 60,
|
||||
cpu: false,
|
||||
usage_cpu: "",
|
||||
memory: false,
|
||||
usage_memory: "",
|
||||
disk: false,
|
||||
usage_disk: "",
|
||||
temperature: false,
|
||||
usage_temperature: "",
|
||||
secret: "",
|
||||
});
|
||||
|
||||
const onChangeForm = (name, value) => {
|
||||
setInfrastructureMonitor({
|
||||
...infrastructureMonitor,
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
const handleCheckboxChange = (event) => {
|
||||
setInfrastructureMonitor({
|
||||
...infrastructureMonitor,
|
||||
[event.target.name]: event.target.checked,
|
||||
});
|
||||
};
|
||||
const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => {
|
||||
const gt = globalSettings?.data?.settings?.globalThresholds || {};
|
||||
setInfrastructureMonitor((prev) => ({
|
||||
...prev,
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
interval: 0.25,
|
||||
cpu: gt.cpu !== undefined,
|
||||
usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "",
|
||||
memory: gt.memory !== undefined,
|
||||
usage_memory: gt.memory !== undefined ? gt.memory.toString() : "",
|
||||
disk: gt.disk !== undefined,
|
||||
usage_disk: gt.disk !== undefined ? gt.disk.toString() : "",
|
||||
temperature: gt.temperature !== undefined,
|
||||
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
|
||||
secret: "",
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { thresholds = {} } = monitor;
|
||||
setInfrastructureMonitor((prev) => ({
|
||||
...prev,
|
||||
url: monitor.url.replace(/^https?:\/\//, ""),
|
||||
name: monitor.name || "",
|
||||
notifications: monitor.notifications || [],
|
||||
interval: monitor.interval / MS_PER_MINUTE,
|
||||
statusWindowSize: monitor.statusWindowSize,
|
||||
statusWindowThreshold: monitor.statusWindowThreshold,
|
||||
cpu: thresholds.usage_cpu !== undefined,
|
||||
usage_cpu:
|
||||
thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "",
|
||||
memory: thresholds.usage_memory !== undefined,
|
||||
usage_memory:
|
||||
thresholds.usage_memory !== undefined
|
||||
? (thresholds.usage_memory * 100).toString()
|
||||
: "",
|
||||
disk: thresholds.usage_disk !== undefined,
|
||||
usage_disk:
|
||||
thresholds.usage_disk !== undefined
|
||||
? (thresholds.usage_disk * 100).toString()
|
||||
: "",
|
||||
temperature: thresholds.usage_temperature !== undefined,
|
||||
usage_temperature:
|
||||
thresholds.usage_temperature !== undefined
|
||||
? (thresholds.usage_temperature * 100).toString()
|
||||
: "",
|
||||
secret: monitor.secret || "",
|
||||
}));
|
||||
}, []);
|
||||
return {
|
||||
infrastructureMonitor,
|
||||
setInfrastructureMonitor,
|
||||
onChangeForm,
|
||||
handleCheckboxChange,
|
||||
initializeInfrastructureMonitorForCreate,
|
||||
initializeInfrastructureMonitorForUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
export default useInfrastructureMonitorForm;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks";
|
||||
const useInfrastructureSubmit = () => {
|
||||
const [createMonitor, isCreating] = useCreateMonitor();
|
||||
const [updateMonitor, isUpdating] = useUpdateMonitor();
|
||||
const buildForm = (infrastructureMonitor, https) => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
|
||||
let form = {
|
||||
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
|
||||
name:
|
||||
infrastructureMonitor.name === ""
|
||||
? infrastructureMonitor.url
|
||||
: infrastructureMonitor.name,
|
||||
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
|
||||
statusWindowSize: infrastructureMonitor.statusWindowSize,
|
||||
statusWindowThreshold: infrastructureMonitor.statusWindowThreshold,
|
||||
cpu: infrastructureMonitor.cpu,
|
||||
...(infrastructureMonitor.cpu
|
||||
? { usage_cpu: infrastructureMonitor.usage_cpu }
|
||||
: {}),
|
||||
memory: infrastructureMonitor.memory,
|
||||
...(infrastructureMonitor.memory
|
||||
? { usage_memory: infrastructureMonitor.usage_memory }
|
||||
: {}),
|
||||
disk: infrastructureMonitor.disk,
|
||||
...(infrastructureMonitor.disk
|
||||
? { usage_disk: infrastructureMonitor.usage_disk }
|
||||
: {}),
|
||||
temperature: infrastructureMonitor.temperature,
|
||||
...(infrastructureMonitor.temperature
|
||||
? { usage_temperature: infrastructureMonitor.usage_temperature }
|
||||
: {}),
|
||||
secret: infrastructureMonitor.secret,
|
||||
};
|
||||
return form;
|
||||
};
|
||||
const submitInfrastructureForm = async (
|
||||
infrastructureMonitor,
|
||||
form,
|
||||
isCreate,
|
||||
monitorId
|
||||
) => {
|
||||
const {
|
||||
cpu,
|
||||
usage_cpu,
|
||||
memory,
|
||||
usage_memory,
|
||||
disk,
|
||||
usage_disk,
|
||||
temperature,
|
||||
usage_temperature,
|
||||
...rest
|
||||
} = form;
|
||||
|
||||
const thresholds = {
|
||||
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
|
||||
...(memory ? { usage_memory: usage_memory / 100 } : {}),
|
||||
...(disk ? { usage_disk: usage_disk / 100 } : {}),
|
||||
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
|
||||
};
|
||||
|
||||
const finalForm = {
|
||||
...(isCreate ? {} : { _id: monitorId }),
|
||||
...rest,
|
||||
description: form.name,
|
||||
type: "hardware",
|
||||
notifications: infrastructureMonitor.notifications,
|
||||
thresholds,
|
||||
};
|
||||
// Handle create or update
|
||||
isCreate
|
||||
? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" })
|
||||
: await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" });
|
||||
};
|
||||
return {
|
||||
buildForm,
|
||||
submitInfrastructureForm,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
};
|
||||
};
|
||||
export default useInfrastructureSubmit;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { infrastructureMonitorValidation } from "../../../../Validation/validation";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
const useValidateInfrastructureForm = () => {
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const validateField = (name, value) => {
|
||||
const { error } = infrastructureMonitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
|
||||
}));
|
||||
};
|
||||
|
||||
const validateForm = (form) => {
|
||||
const { error } = infrastructureMonitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
console.log(newErrors);
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Please check the form for errors." });
|
||||
return error;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return { errors, validateField, validateForm };
|
||||
};
|
||||
export default useValidateInfrastructureForm;
|
||||
@@ -1,241 +1,141 @@
|
||||
// React, Redux, Router
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
// Utility and Network
|
||||
import { infrastructureMonitorValidation } from "../../../Validation/validation";
|
||||
import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks";
|
||||
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
|
||||
import NotificationsConfig from "../../../Components/NotificationConfig";
|
||||
import { useUpdateMonitor, useCreateMonitor } from "../../../Hooks/monitorHooks";
|
||||
|
||||
// MUI
|
||||
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
|
||||
//Components
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import Link from "../../../Components/Link";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
|
||||
import Link from "../../../Components/Link";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { CustomThreshold } from "./Components/CustomThreshold";
|
||||
|
||||
const SELECT_VALUES = [
|
||||
{ _id: 0.25, name: "15 seconds" },
|
||||
{ _id: 0.5, name: "30 seconds" },
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
];
|
||||
|
||||
const METRICS = ["cpu", "memory", "disk", "temperature"];
|
||||
const METRIC_PREFIX = "usage_";
|
||||
const MS_PER_MINUTE = 60000;
|
||||
|
||||
const hasAlertError = (errors) => {
|
||||
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
|
||||
};
|
||||
|
||||
const getAlertError = (errors) => {
|
||||
return Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))
|
||||
? errors[Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))]
|
||||
: null;
|
||||
};
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import MonitorStatusHeader from "./Components/MonitorStatusHeader";
|
||||
import MonitorActionButtons from "./Components/MonitorActionButtons";
|
||||
import CustomAlertsSection from "./Components/CustomAlertsSection";
|
||||
// Utils
|
||||
import NotificationsConfig from "../../../Components/NotificationConfig";
|
||||
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useDeleteMonitor,
|
||||
useFetchGlobalSettings,
|
||||
useFetchHardwareMonitorById,
|
||||
usePauseMonitor,
|
||||
} from "../../../Hooks/monitorHooks";
|
||||
import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm";
|
||||
import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm";
|
||||
import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit";
|
||||
|
||||
const CreateInfrastructureMonitor = () => {
|
||||
const theme = useTheme();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const { monitorId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Determine if we are creating or editing
|
||||
const isCreate = typeof monitorId === "undefined";
|
||||
|
||||
// Fetch monitor details if editing
|
||||
const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({ monitorId });
|
||||
const [notifications, notificationsAreLoading, notificationsError] =
|
||||
useGetNotificationsByTeamId();
|
||||
const [updateMonitor, isUpdating] = useUpdateMonitor();
|
||||
const [createMonitor, isCreating] = useCreateMonitor();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// State
|
||||
const [errors, setErrors] = useState({});
|
||||
const [https, setHttps] = useState(false);
|
||||
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
notify_email: false,
|
||||
interval: 0.25,
|
||||
cpu: false,
|
||||
usage_cpu: "",
|
||||
memory: false,
|
||||
usage_memory: "",
|
||||
disk: false,
|
||||
usage_disk: "",
|
||||
temperature: false,
|
||||
usage_temperature: "",
|
||||
secret: "",
|
||||
});
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
// Fetch monitor details if editing
|
||||
const [monitor, isLoading] = useFetchHardwareMonitorById({
|
||||
monitorId,
|
||||
updateTrigger,
|
||||
});
|
||||
const [deleteMonitor, isDeleting] = useDeleteMonitor();
|
||||
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
|
||||
const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId();
|
||||
const [pauseMonitor, isPausing] = usePauseMonitor();
|
||||
const {
|
||||
infrastructureMonitor,
|
||||
setInfrastructureMonitor,
|
||||
onChangeForm,
|
||||
handleCheckboxChange,
|
||||
initializeInfrastructureMonitorForCreate,
|
||||
initializeInfrastructureMonitorForUpdate,
|
||||
} = useInfrastructureMonitorForm();
|
||||
const { errors, validateField, validateForm } = useValidateInfrastructureForm();
|
||||
const { buildForm, submitInfrastructureForm, isCreating, isUpdating } =
|
||||
useInfrastructureSubmit();
|
||||
|
||||
const FREQUENCIES = [
|
||||
{ _id: 0.25, name: t("time.fifteenSeconds") },
|
||||
{ _id: 0.5, name: t("time.thirtySeconds") },
|
||||
{ _id: 1, name: t("time.oneMinute") },
|
||||
{ _id: 2, name: t("time.twoMinutes") },
|
||||
{ _id: 5, name: t("time.fiveMinutes") },
|
||||
{ _id: 10, name: t("time.tenMinutes") },
|
||||
];
|
||||
const CRUMBS = [
|
||||
{ name: "Infrastructure monitors", path: "/infrastructure" },
|
||||
...(isCreate
|
||||
? [{ name: "Create", path: "/infrastructure/create" }]
|
||||
: [
|
||||
{ name: "Details", path: `/infrastructure/${monitorId}` },
|
||||
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
|
||||
]),
|
||||
];
|
||||
// Populate form fields if editing
|
||||
useEffect(() => {
|
||||
if (isCreate || !monitor) return;
|
||||
|
||||
setInfrastructureMonitor({
|
||||
url: monitor.url.replace(/^https?:\/\//, ""),
|
||||
name: monitor.name || "",
|
||||
notifications: monitor.notifications,
|
||||
interval: monitor.interval / MS_PER_MINUTE,
|
||||
cpu: monitor.thresholds?.usage_cpu !== undefined,
|
||||
usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "",
|
||||
|
||||
memory: monitor.thresholds?.usage_memory !== undefined,
|
||||
usage_memory: monitor.thresholds?.usage_memory
|
||||
? monitor.thresholds.usage_memory * 100
|
||||
: "",
|
||||
|
||||
disk: monitor.thresholds?.usage_disk !== undefined,
|
||||
usage_disk: monitor.thresholds?.usage_disk
|
||||
? monitor.thresholds.usage_disk * 100
|
||||
: "",
|
||||
|
||||
temperature: monitor.thresholds?.usage_temperature !== undefined,
|
||||
usage_temperature: monitor.thresholds?.usage_temperature
|
||||
? monitor.thresholds.usage_temperature * 100
|
||||
: "",
|
||||
secret: monitor.secret || "",
|
||||
});
|
||||
setHttps(monitor.url.startsWith("https"));
|
||||
}, [isCreate, monitor]);
|
||||
if (isCreate) {
|
||||
if (globalSettingsLoading) return;
|
||||
setHttps(false);
|
||||
initializeInfrastructureMonitorForCreate(globalSettings);
|
||||
} else if (monitor) {
|
||||
setHttps(monitor.url.startsWith("https"));
|
||||
initializeInfrastructureMonitorForUpdate(monitor);
|
||||
}
|
||||
}, [
|
||||
isCreate,
|
||||
monitor,
|
||||
globalSettings,
|
||||
globalSettingsLoading,
|
||||
initializeInfrastructureMonitorForCreate,
|
||||
initializeInfrastructureMonitorForUpdate,
|
||||
]);
|
||||
|
||||
// Handlers
|
||||
const onSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// Build the form
|
||||
let form = {
|
||||
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
|
||||
name:
|
||||
infrastructureMonitor.name === ""
|
||||
? infrastructureMonitor.url
|
||||
: infrastructureMonitor.name,
|
||||
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
|
||||
cpu: infrastructureMonitor.cpu,
|
||||
...(infrastructureMonitor.cpu
|
||||
? { usage_cpu: infrastructureMonitor.usage_cpu }
|
||||
: {}),
|
||||
memory: infrastructureMonitor.memory,
|
||||
...(infrastructureMonitor.memory
|
||||
? { usage_memory: infrastructureMonitor.usage_memory }
|
||||
: {}),
|
||||
disk: infrastructureMonitor.disk,
|
||||
...(infrastructureMonitor.disk
|
||||
? { usage_disk: infrastructureMonitor.usage_disk }
|
||||
: {}),
|
||||
temperature: infrastructureMonitor.temperature,
|
||||
...(infrastructureMonitor.temperature
|
||||
? { usage_temperature: infrastructureMonitor.usage_temperature }
|
||||
: {}),
|
||||
secret: infrastructureMonitor.secret,
|
||||
};
|
||||
|
||||
const { error } = infrastructureMonitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
const form = buildForm(infrastructureMonitor, https);
|
||||
const error = validateForm(form);
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
console.log(newErrors);
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Please check the form for errors." });
|
||||
return;
|
||||
}
|
||||
submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId);
|
||||
};
|
||||
|
||||
// Build the thresholds for the form
|
||||
const {
|
||||
cpu,
|
||||
usage_cpu,
|
||||
memory,
|
||||
usage_memory,
|
||||
disk,
|
||||
usage_disk,
|
||||
temperature,
|
||||
usage_temperature,
|
||||
...rest
|
||||
} = form;
|
||||
|
||||
const thresholds = {
|
||||
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
|
||||
...(memory ? { usage_memory: usage_memory / 100 } : {}),
|
||||
...(disk ? { usage_disk: usage_disk / 100 } : {}),
|
||||
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
|
||||
};
|
||||
|
||||
form = {
|
||||
...(isCreate ? {} : { _id: monitorId }),
|
||||
...rest,
|
||||
description: form.name,
|
||||
type: "hardware",
|
||||
notifications: infrastructureMonitor.notifications,
|
||||
thresholds,
|
||||
};
|
||||
|
||||
// Handle create or update
|
||||
isCreate
|
||||
? await createMonitor({ monitor: form, redirect: "/infrastructure" })
|
||||
: await updateMonitor({ monitor: form, redirect: "/infrastructure" });
|
||||
const triggerUpdate = () => {
|
||||
setUpdateTrigger(!updateTrigger);
|
||||
};
|
||||
|
||||
const onChange = (event) => {
|
||||
const { value, name } = event.target;
|
||||
setInfrastructureMonitor({
|
||||
...infrastructureMonitor,
|
||||
[name]: value,
|
||||
});
|
||||
|
||||
const { error } = infrastructureMonitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
|
||||
}));
|
||||
onChangeForm(name, value);
|
||||
validateField(name, value);
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (event) => {
|
||||
const { name } = event.target;
|
||||
const { checked } = event.target;
|
||||
setInfrastructureMonitor({
|
||||
...infrastructureMonitor,
|
||||
[name]: checked,
|
||||
});
|
||||
const handlePause = async () => {
|
||||
await pauseMonitor({ monitorId, triggerUpdate });
|
||||
};
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
await deleteMonitor({ monitor, redirect: "/infrastructure" });
|
||||
};
|
||||
|
||||
const isBusy =
|
||||
isLoading ||
|
||||
isUpdating ||
|
||||
isCreating ||
|
||||
isDeleting ||
|
||||
isPausing ||
|
||||
notificationsAreLoading;
|
||||
|
||||
return (
|
||||
<Box className="create-infrastructure-monitor">
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "Infrastructure monitors", path: "/infrastructure" },
|
||||
...(isCreate
|
||||
? [{ name: "Create", path: "/infrastructure/create" }]
|
||||
: [
|
||||
{ name: "Details", path: `/infrastructure/${monitorId}` },
|
||||
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
|
||||
]),
|
||||
]}
|
||||
/>
|
||||
<Breadcrumbs list={CRUMBS} />
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={onSubmit}
|
||||
@@ -244,27 +144,55 @@ const CreateInfrastructureMonitor = () => {
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
>
|
||||
{t(isCreate ? "infrastructureCreateYour" : "infrastructureEditYour")}{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h2"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
{t("monitor")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={
|
||||
!isCreate ? theme.palette.primary.contrastTextSecondary : undefined
|
||||
}
|
||||
>
|
||||
{!isCreate ? infrastructureMonitor.name : t("createYour") + " "}
|
||||
</Typography>
|
||||
{isCreate ? (
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("monitor")}
|
||||
</Typography>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Typography>
|
||||
{!isCreate && (
|
||||
<MonitorStatusHeader
|
||||
monitor={monitor}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{!isCreate && (
|
||||
<MonitorActionButtons
|
||||
monitor={monitor}
|
||||
isBusy={isBusy}
|
||||
handlePause={handlePause}
|
||||
handleRemove={handleRemove}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Stack>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
@@ -283,7 +211,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
/>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<TextInput
|
||||
type="url"
|
||||
id="url"
|
||||
@@ -299,8 +227,10 @@ const CreateInfrastructureMonitor = () => {
|
||||
disabled={!isCreate}
|
||||
/>
|
||||
{isCreate && (
|
||||
<Box>
|
||||
<Typography component="p">{t("infrastructureProtocol")}</Typography>
|
||||
<FieldWrapper
|
||||
label={t("infrastructureProtocol")}
|
||||
labelVariant="p"
|
||||
>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="group"
|
||||
@@ -317,7 +247,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
{t("http")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
</FieldWrapper>
|
||||
)}
|
||||
<TextInput
|
||||
type="text"
|
||||
@@ -344,7 +274,12 @@ const CreateInfrastructureMonitor = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("notificationConfig.title")}</Typography>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("notificationConfig.title")}
|
||||
</Typography>
|
||||
<Typography component="p">{t("notificationConfig.description")}</Typography>
|
||||
</Box>
|
||||
<NotificationsConfig
|
||||
@@ -359,52 +294,39 @@ const CreateInfrastructureMonitor = () => {
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("infrastructureCustomizeAlerts")}
|
||||
{t("createMonitorPage.incidentConfigTitle")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("infrastructureAlertNotificationDescription")}
|
||||
{t("createMonitorPage.incidentConfigDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
{METRICS.map((metric) => {
|
||||
return (
|
||||
<CustomThreshold
|
||||
key={metric}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
errors={errors}
|
||||
checkboxId={metric}
|
||||
checkboxName={metric}
|
||||
checkboxLabel={
|
||||
metric !== "cpu"
|
||||
? capitalizeFirstLetter(metric)
|
||||
: metric.toUpperCase()
|
||||
}
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
isChecked={infrastructureMonitor[metric]}
|
||||
fieldId={METRIC_PREFIX + metric}
|
||||
fieldName={METRIC_PREFIX + metric}
|
||||
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
|
||||
onFieldChange={onChange}
|
||||
alertUnit={metric == "temperature" ? "°C" : "%"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Error text */}
|
||||
{hasAlertError(errors) && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{getAlertError(errors)}
|
||||
</Typography>
|
||||
)}
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<TextInput
|
||||
name="statusWindowSize"
|
||||
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
|
||||
type="number"
|
||||
value={infrastructureMonitor.statusWindowSize}
|
||||
onChange={onChange}
|
||||
error={errors["statusWindowSize"] ? true : false}
|
||||
helperText={errors["statusWindowSize"]}
|
||||
/>
|
||||
<TextInput
|
||||
name="statusWindowThreshold"
|
||||
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
|
||||
type="number"
|
||||
value={infrastructureMonitor.statusWindowThreshold}
|
||||
onChange={onChange}
|
||||
error={errors["statusWindowThreshold"] ? true : false}
|
||||
helperText={errors["statusWindowThreshold"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<CustomAlertsSection
|
||||
errors={errors}
|
||||
onChange={onChange}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
@@ -421,7 +343,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
label="Check frequency"
|
||||
value={infrastructureMonitor.interval || 15}
|
||||
onChange={onChange}
|
||||
items={SELECT_VALUES}
|
||||
items={FREQUENCIES}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@@ -433,7 +355,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isLoading || isUpdating || isCreating || notificationsAreLoading}
|
||||
loading={isBusy}
|
||||
>
|
||||
{t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")}
|
||||
</Button>
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
import { Stack } from "@mui/material";
|
||||
import Gauge from "./Gauge";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
// Utils
|
||||
import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Gauges = ({ shouldRender, monitor }) => {
|
||||
const Gauges = ({ isLoading = false, monitor }) => {
|
||||
const { decimalToPercentage, formatBytes } = useHardwareUtils();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!shouldRender) {
|
||||
if (isLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
@@ -60,6 +61,7 @@ const Gauges = ({ shouldRender, monitor }) => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
{gauges.map((gauge) => {
|
||||
@@ -79,4 +81,9 @@ const Gauges = ({ shouldRender, monitor }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Gauges.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Gauges;
|
||||
Loaded 100 of 400 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user