Compare commits
674
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f92dc835b7 | ||
|
|
0b0d412187 | ||
|
|
77b817778b | ||
|
|
eddb9a82a8 | ||
|
|
9459edce00 | ||
|
|
eaf1064413 | ||
|
|
dae6f9a570 | ||
|
|
9fa45c5871 | ||
|
|
2810cd3562 | ||
|
|
f4177baed5 | ||
|
|
5feb666bdb | ||
|
|
8396f478c5 | ||
|
|
bbfd963deb | ||
|
|
fc0cda3da6 | ||
|
|
861d867a17 | ||
|
|
8d7c322279 | ||
|
|
68a9bece1e | ||
|
|
4f82a11c66 | ||
|
|
cc1814f507 | ||
|
|
436c11bcba | ||
|
|
d5ec2936ad | ||
|
|
937ffe4f3b | ||
|
|
0df71d6356 | ||
|
|
adba25269c | ||
|
|
386a384ef6 | ||
|
|
8bf5db0834 | ||
|
|
817d01c857 | ||
|
|
3f30acdbdf | ||
|
|
80b604b9ff | ||
|
|
9356b0d482 | ||
|
|
6699a83948 | ||
|
|
900c039672 | ||
|
|
a4ab7d662e | ||
|
|
807d7e10c3 | ||
|
|
39043f3945 | ||
|
|
5a72e1e123 | ||
|
|
959b150ebf | ||
|
|
552b7a037d | ||
|
|
8671685cf9 | ||
|
|
ff536f40a2 | ||
|
|
e3d63a1cb4 | ||
|
|
105d7f0580 | ||
|
|
e2a1f02971 | ||
|
|
b7f548ff15 | ||
|
|
29d05c1e57 | ||
|
|
0ec77a5ab1 | ||
|
|
95eb75f416 | ||
|
|
8f657dcd36 | ||
|
|
c0e33eb4fa | ||
|
|
754b0af3a3 | ||
|
|
a39792cf0b | ||
|
|
35c9618d59 | ||
|
|
c6c41c2e3e | ||
|
|
f58825d19f | ||
|
|
e4649f3d76 | ||
|
|
c7361666f5 | ||
|
|
3f5707721f | ||
|
|
cc5f033d0d | ||
|
|
d2d0fb788e | ||
|
|
9d969d67ca | ||
|
|
6e1c26b987 | ||
|
|
ef353dcb36 | ||
|
|
b21ef60cda | ||
|
|
f43bf9ab3d | ||
|
|
48ca64ec6f | ||
|
|
e036c6077e | ||
|
|
c9f0254aed | ||
|
|
b3d9c5b94c | ||
|
|
fd536541d7 | ||
|
|
7a82c76d5c | ||
|
|
5fab2d0a30 | ||
|
|
860b2d6214 | ||
|
|
8579edd56e | ||
|
|
969d24e369 | ||
|
|
52b95ad121 | ||
|
|
ea23610b3a | ||
|
|
a4cc322a55 | ||
|
|
2b9ff52f6b | ||
|
|
0f230c4d70 | ||
|
|
74783fd738 | ||
|
|
71d3e789ff | ||
|
|
f59a7c3113 | ||
|
|
ffbf223288 | ||
|
|
96021b7479 | ||
|
|
f0a5974eaf | ||
|
|
c9b8f434cd | ||
|
|
3685e0c476 | ||
|
|
d6f9fca907 | ||
|
|
ba943e90c2 | ||
|
|
dead97169f | ||
|
|
eb1d307932 | ||
|
|
a31dd2f19e | ||
|
|
d84fc7e299 | ||
|
|
8afd67a06d | ||
|
|
71a4fddffd | ||
|
|
3489ee000d | ||
|
|
93b9c5033a | ||
|
|
32ec909bf7 | ||
|
|
8e5757beb1 | ||
|
|
8e5a337f3e | ||
|
|
9db4e557b3 | ||
|
|
10174da4f0 | ||
|
|
99ded83bd5 | ||
|
|
dffa49ae9d | ||
|
|
89c86a7628 | ||
|
|
09446c9fc1 | ||
|
|
755134ee7f | ||
|
|
3b919044e6 | ||
|
|
08e9c02a3a | ||
|
|
85f674ee08 | ||
|
|
16036f421b | ||
|
|
7fe7b79103 | ||
|
|
a058c0518c | ||
|
|
5ed9ab780a | ||
|
|
e32590425b | ||
|
|
190a3b47da | ||
|
|
f740eef0fa | ||
|
|
5efd64d44d | ||
|
|
b28c84436e | ||
|
|
9082cf0826 | ||
|
|
9d453bdb0b | ||
|
|
39d92fb067 | ||
|
|
a54fdd89ab | ||
|
|
6aa9360b98 | ||
|
|
bb3f471d19 | ||
|
|
6d433fd0ac | ||
|
|
d8dafa311a | ||
|
|
398e5d7314 | ||
|
|
53ee45172d | ||
|
|
1ea6dafb1a | ||
|
|
9785dc146e | ||
|
|
1b1d8ea229 | ||
|
|
603bd1d814 | ||
|
|
158d976ccb | ||
|
|
380fdf470f | ||
|
|
1e2a4b4a84 | ||
|
|
5e0d8f0550 | ||
|
|
bffc4ee813 | ||
|
|
ad9ff7f5df | ||
|
|
8277c4ed13 | ||
|
|
f65238da6c | ||
|
|
64b8aab4d6 | ||
|
|
7244b35091 | ||
|
|
699883e0b1 | ||
|
|
f7211711ce | ||
|
|
cb0dd878d6 | ||
|
|
8c5be9ef00 | ||
|
|
d1b6d08c73 | ||
|
|
2d68e96782 | ||
|
|
306f2a60a2 | ||
|
|
abf6dc2908 | ||
|
|
7ce6068757 | ||
|
|
a7d9bb822e | ||
|
|
142ecaaf8b | ||
|
|
90bbc764fc | ||
|
|
ae4c433246 | ||
|
|
d2461ef118 | ||
|
|
47162f2064 | ||
|
|
8b9c41464a | ||
|
|
51b6ec1751 | ||
|
|
6165ae60e6 | ||
|
|
69e142f711 | ||
|
|
eff4f8ad69 | ||
|
|
bca902471d | ||
|
|
1b3f2f0365 | ||
|
|
be12991997 | ||
|
|
54596fe776 | ||
|
|
8d637dbcac | ||
|
|
771fd5b50f | ||
|
|
8dedf2a742 | ||
|
|
c882daf000 | ||
|
|
1aa7ae8f11 | ||
|
|
517139cbf4 | ||
|
|
ef049b6521 | ||
|
|
33f8641f3a | ||
|
|
2f22a52116 | ||
|
|
8af13b04ec | ||
|
|
ac77216f07 | ||
|
|
a43363ebfc | ||
|
|
0ca738b080 | ||
|
|
091c36cbf3 | ||
|
|
120708ed47 | ||
|
|
6a1ab622b3 | ||
|
|
0aef8b652f | ||
|
|
20863fbce7 | ||
|
|
fa7eaa4788 | ||
|
|
ee9c08b1bb | ||
|
|
7ee30e12f5 | ||
|
|
c5f57eda62 | ||
|
|
2bf3c08f3c | ||
|
|
187c8eaba1 | ||
|
|
25d8839441 | ||
|
|
0f4240d3db | ||
|
|
a37bc63f31 | ||
|
|
73f95ac8e1 | ||
|
|
c8c4bac2c7 | ||
|
|
9fecfa562f | ||
|
|
7991f3f6a1 | ||
|
|
692d4d89d6 | ||
|
|
f1b104b78d | ||
|
|
3c2b571313 | ||
|
|
abec4e6c70 | ||
|
|
d022f7d849 | ||
|
|
0fb210e9e1 | ||
|
|
ce4cb63035 | ||
|
|
f637b01631 | ||
|
|
e390bd764f | ||
|
|
30e8cfc9ec | ||
|
|
80d6868a1d | ||
|
|
942d75afcd | ||
|
|
c34fcc5cb3 | ||
|
|
409f9f6dc6 | ||
|
|
525206c1c1 | ||
|
|
9a85389b8c | ||
|
|
eeac784922 | ||
|
|
19b873ca87 | ||
|
|
0f99c7004d | ||
|
|
6a566c3693 | ||
|
|
dfcf0309fe | ||
|
|
395b7fbe2a | ||
|
|
73b74ffd80 | ||
|
|
2b393f9ebe | ||
|
|
562a17707d | ||
|
|
7d800b0f38 | ||
|
|
a19479b578 | ||
|
|
9b4a810e21 | ||
|
|
1388d61b07 | ||
|
|
8dbe42288b | ||
|
|
d2c13fac5a | ||
|
|
dd16ac060a | ||
|
|
5628c202f7 | ||
|
|
45e31e7212 | ||
|
|
7872b18108 | ||
|
|
15ac5fa84d | ||
|
|
3c85b36b1b | ||
|
|
ee0bc5e500 | ||
|
|
4729863826 | ||
|
|
174fb048f7 | ||
|
|
63fd3f6afd | ||
|
|
c3944f2fe7 | ||
|
|
ddd937d42f | ||
|
|
5e225f9173 | ||
|
|
d2d110fcd5 | ||
|
|
372fc9869b | ||
|
|
724f30070d | ||
|
|
b8a44f09fc | ||
|
|
f85f1aa1d1 | ||
|
|
707b8f903b | ||
|
|
9e4fd6d708 | ||
|
|
8572065ef7 | ||
|
|
01c3870b22 | ||
|
|
420c103f41 | ||
|
|
b4970d321d | ||
|
|
f5a4a03898 | ||
|
|
dfae6c8abd | ||
|
|
2b6734bb78 | ||
|
|
b9fd29b906 | ||
|
|
caf8f22fec | ||
|
|
599c599588 | ||
|
|
cfed235d1f | ||
|
|
c9eacff71f | ||
|
|
b2526a5be9 | ||
|
|
cffe3f57fa | ||
|
|
8b2506cc3d | ||
|
|
60356a95fe | ||
|
|
1195567da8 | ||
|
|
7617929700 | ||
|
|
7df3ee6ecd | ||
|
|
dc788dbf85 | ||
|
|
89c4f4005e | ||
|
|
53ed3b3465 | ||
|
|
e37fbf2a5c | ||
|
|
4df08bfa79 | ||
|
|
f74231cf33 | ||
|
|
e1558d35b0 | ||
|
|
51672890df | ||
|
|
fc357fe3ae | ||
|
|
1a0e6d9404 | ||
|
|
87a6640c1d | ||
|
|
4e31499f78 | ||
|
|
b9e8a6dbc8 | ||
|
|
c704f67e57 | ||
|
|
4629a947a4 | ||
|
|
da92b9d067 | ||
|
|
458442f364 | ||
|
|
400b295dae | ||
|
|
6ddee6e85b | ||
|
|
2db88ac192 | ||
|
|
0342870bbd | ||
|
|
4280a6c2c3 | ||
|
|
0a3232419a | ||
|
|
4dc3a756a8 | ||
|
|
cd91e83817 | ||
|
|
f476710c29 | ||
|
|
0d255c8962 | ||
|
|
bf25a65d8e | ||
|
|
f1e16b65ee | ||
|
|
201ad987ce | ||
|
|
c12ce2fbcf | ||
|
|
6e77738ecf | ||
|
|
cbd1153189 | ||
|
|
88042e7c50 | ||
|
|
ba44985763 | ||
|
|
b164fa63bf | ||
|
|
7630491ad4 | ||
|
|
09be4cdf9a | ||
|
|
8360c05448 | ||
|
|
3cce1aa842 | ||
|
|
8a83bc99e9 | ||
|
|
37dfc67e33 | ||
|
|
4a939ad2c4 | ||
|
|
56ee8ef5ea | ||
|
|
8dd13c4b75 | ||
|
|
6b5c3bba47 | ||
|
|
02998ed06e | ||
|
|
397541a789 | ||
|
|
949beff4f9 | ||
|
|
806c7764b1 | ||
|
|
d18a53eec2 | ||
|
|
f4b14e7df9 | ||
|
|
e834402672 | ||
|
|
290adf024c | ||
|
|
2c89086b68 | ||
|
|
20167d537b | ||
|
|
bf43632482 | ||
|
|
2d432e0a3d | ||
|
|
967e415b0a | ||
|
|
2764468e90 | ||
|
|
10d0835eb3 | ||
|
|
1702fd1c5a | ||
|
|
c98ec7d047 | ||
|
|
9ffe0caed9 | ||
|
|
ef8a8016df | ||
|
|
3a1a9bde4d | ||
|
|
0e24bb7c94 | ||
|
|
a415c75eb4 | ||
|
|
d1f46aba9e | ||
|
|
bb60d22dea | ||
|
|
0ff642cb24 | ||
|
|
a044d7b2c7 | ||
|
|
9cee2eebd0 | ||
|
|
bec5ef09cb | ||
|
|
3abacb046b | ||
|
|
fbfb861447 | ||
|
|
0470eab782 | ||
|
|
abf66eed95 | ||
|
|
4f571a258c | ||
|
|
85196852f4 | ||
|
|
46c80985ec | ||
|
|
8c7b691fda | ||
|
|
d5dac6b557 | ||
|
|
f0129f257a | ||
|
|
4b9f1ba43b | ||
|
|
1322033d41 | ||
|
|
7c201d087c | ||
|
|
27e34f3b95 | ||
|
|
130d825d66 | ||
|
|
c6d82223f9 | ||
|
|
3bcdd3c445 | ||
|
|
3f290bc963 | ||
|
|
7d7926fab9 | ||
|
|
a09a885786 | ||
|
|
76953f9a5c | ||
|
|
67bba1479c | ||
|
|
b981427d8d | ||
|
|
f37ae8fddb | ||
|
|
dc80b3cafc | ||
|
|
896d1b784b | ||
|
|
02640c3155 | ||
|
|
5a187cd1c9 | ||
|
|
e6a0f89c68 | ||
|
|
e96de364c0 | ||
|
|
8aa55885e5 | ||
|
|
1f7344f2e6 | ||
|
|
fe74d4b465 | ||
|
|
9c62dc9c52 | ||
|
|
2a2f101d4b | ||
|
|
76ce362eea | ||
|
|
029f732a86 | ||
|
|
a7abbd60bd | ||
|
|
9b84b98391 | ||
|
|
6c2e3b4fe1 | ||
|
|
57acbd3497 | ||
|
|
48735c7dc8 | ||
|
|
451c752170 | ||
|
|
b1871b31a5 | ||
|
|
a740a9e005 | ||
|
|
43ff7ca4bd | ||
|
|
01a53298c2 | ||
|
|
3de392c3c9 | ||
|
|
d1b5830c3d | ||
|
|
704f25b987 | ||
|
|
f46c2edd1b | ||
|
|
9d7523c406 | ||
|
|
bc7b3a5ffb | ||
|
|
72f23ca025 | ||
|
|
ae9416641c | ||
|
|
b7844f8b10 | ||
|
|
ffb563f36d | ||
|
|
ae5115a38f | ||
|
|
b925434078 | ||
|
|
f713698572 | ||
|
|
47d2b62e92 | ||
|
|
780189e4a1 | ||
|
|
f86ceae0c6 | ||
|
|
e78524d1c9 | ||
|
|
7a2dab6cc4 | ||
|
|
c90442b49f | ||
|
|
bdecc1a5e2 | ||
|
|
c1fc537479 | ||
|
|
6f0540fb33 | ||
|
|
3876aae484 | ||
|
|
2f646ee154 | ||
|
|
01da5847f0 | ||
|
|
4b888c1ed1 | ||
|
|
6b63091d89 | ||
|
|
78fdc548bb | ||
|
|
bbdbe75255 | ||
|
|
74bc5accb6 | ||
|
|
60770a4544 | ||
|
|
766d847561 | ||
|
|
08043a5ea0 | ||
|
|
272e4b5014 | ||
|
|
98d2bbfbb9 | ||
|
|
427f8c632f | ||
|
|
9a004ab1fb | ||
|
|
c9b9c4a53f | ||
|
|
db746b0ff2 | ||
|
|
80fca45ce8 | ||
|
|
182ff390e9 | ||
|
|
fc711e8e24 | ||
|
|
0f662e1a65 | ||
|
|
4b4fae91c2 | ||
|
|
172d36ba1d | ||
|
|
0c36c8e625 | ||
|
|
2a7bde9574 | ||
|
|
cb9cc101c0 | ||
|
|
06ef850577 | ||
|
|
0797008ac9 | ||
|
|
fda9d9edfe | ||
|
|
c6d86d78b8 | ||
|
|
0d72071019 | ||
|
|
84533119c9 | ||
|
|
60cded78a6 | ||
|
|
c45bcfaa2f | ||
|
|
747f31477d | ||
|
|
713c3ea34f | ||
|
|
1d90b8d8d9 | ||
|
|
0827d4f528 | ||
|
|
7c14d81a24 | ||
|
|
7906f2f615 | ||
|
|
b462ba7562 | ||
|
|
bd059da94a | ||
|
|
4db95cfa44 | ||
|
|
602769b3e7 | ||
|
|
a69f8a5ab5 | ||
|
|
940fc38056 | ||
|
|
bf369aa546 | ||
|
|
c72287cb21 | ||
|
|
ba2d23fd82 | ||
|
|
e725128b10 | ||
|
|
baf7344e2d | ||
|
|
387cb35494 | ||
|
|
6a8f97ae65 | ||
|
|
3365a8f7da | ||
|
|
2e9b87ba32 | ||
|
|
1fd56610ea | ||
|
|
22230b8a08 | ||
|
|
f648ab1b24 | ||
|
|
281fc99136 | ||
|
|
6f0cc307dc | ||
|
|
cb218a46a9 | ||
|
|
39807c2de1 | ||
|
|
59d97f0d25 | ||
|
|
657a35e948 | ||
|
|
1b0b17b589 | ||
|
|
ff34c872c8 | ||
|
|
1458cf4e4a | ||
|
|
cb285bbb66 | ||
|
|
13b13b6818 | ||
|
|
63c1a60b06 | ||
|
|
53c58eb945 | ||
|
|
d5cd957bbd | ||
|
|
5b25666054 | ||
|
|
34feb6d3fa | ||
|
|
a57da284d7 | ||
|
|
3f9019f34c | ||
|
|
4e284c1468 | ||
|
|
6b9759e455 | ||
|
|
094def117a | ||
|
|
28baeaf218 | ||
|
|
c535ba8c1a | ||
|
|
4c04294f71 | ||
|
|
212c1fec52 | ||
|
|
05798914a8 | ||
|
|
6a4d8a3f6b | ||
|
|
e906b237a0 | ||
|
|
bacbf8404e | ||
|
|
d47cc65fed | ||
|
|
e077fe52e7 | ||
|
|
f39029a37f | ||
|
|
c8c54ce49e | ||
|
|
021eda098d | ||
|
|
8993ecadce | ||
|
|
f92e0fae3a | ||
|
|
a4b73bc4ed | ||
|
|
87b7ff7452 | ||
|
|
61de273a73 | ||
|
|
04332c47d5 | ||
|
|
f263c78a4d | ||
|
|
de67ea9ee6 | ||
|
|
c9c6c42116 | ||
|
|
79d2eb68fa | ||
|
|
bc9441bcd0 | ||
|
|
d868f6931d | ||
|
|
29dbebd228 | ||
|
|
906d306827 | ||
|
|
aaf7d7e47d | ||
|
|
c40f4e9f1c | ||
|
|
35daed605f | ||
|
|
6434afe2c9 | ||
|
|
788ca78234 | ||
|
|
3adadf0d07 | ||
|
|
55b9702fc3 | ||
|
|
7f4282ab1f | ||
|
|
fcf2557a2f | ||
|
|
bdf1e13ce8 | ||
|
|
4de2ba4b25 | ||
|
|
d5bf45e2a9 | ||
|
|
6f988cc688 | ||
|
|
8fbbb1dd4b | ||
|
|
baa4d693ae | ||
|
|
e11553c218 | ||
|
|
e71892d0bc | ||
|
|
100e569aa8 | ||
|
|
343167012e | ||
|
|
5330fc3183 | ||
|
|
df7e348366 | ||
|
|
b2c3e08db3 | ||
|
|
8ca62db317 | ||
|
|
3bb767b780 | ||
|
|
20d27cb984 | ||
|
|
0322a96090 | ||
|
|
3dbad1743a | ||
|
|
f4c3ee16db | ||
|
|
a7d5a84206 | ||
|
|
e86734cee9 | ||
|
|
cc84c37a2d | ||
|
|
e85e9dc2d4 | ||
|
|
1398ab6721 | ||
|
|
d32ea7f8c3 | ||
|
|
edeed00be6 | ||
|
|
4c7e96122f | ||
|
|
45c02e0f7f | ||
|
|
aa7051095a | ||
|
|
fcb4fbf277 | ||
|
|
74a12754b6 | ||
|
|
fe8213f8bb | ||
|
|
cf5691e250 | ||
|
|
f3b4b908fd | ||
|
|
f58456961b | ||
|
|
91bdc662ca | ||
|
|
53e04e6af2 | ||
|
|
301942d0eb | ||
|
|
29bb1615f5 | ||
|
|
afc3ad237f | ||
|
|
e0ac040063 | ||
|
|
f895125145 | ||
|
|
63031e938e | ||
|
|
4b82129199 | ||
|
|
ccd176ba0b | ||
|
|
2dd9218910 | ||
|
|
5792ca624e | ||
|
|
5716b5eea4 | ||
|
|
c89dbebe98 | ||
|
|
5d5d1dbb23 | ||
|
|
93505f006c | ||
|
|
a6204b7bf5 | ||
|
|
9c21d71c44 | ||
|
|
fef3f2c294 | ||
|
|
a617bd340a | ||
|
|
23061e31d1 | ||
|
|
34b1a9cdc2 | ||
|
|
bab65770ce | ||
|
|
ed3645c3d0 | ||
|
|
0654e4adfa | ||
|
|
54c0e089a8 | ||
|
|
aee81a03fa | ||
|
|
40bcf7f6ac | ||
|
|
e908d34b3a | ||
|
|
a706e68c6a | ||
|
|
a2786a77ec | ||
|
|
a5af4e48df | ||
|
|
d80d939bd8 | ||
|
|
5874efff3c | ||
|
|
ab9e722caa | ||
|
|
4f8f2f1422 | ||
|
|
c6c50063d8 | ||
|
|
b95d8886b9 | ||
|
|
163f5ec1ec | ||
|
|
959e2ff7b8 | ||
|
|
2f8765cf3e | ||
|
|
49fcab2ee3 | ||
|
|
5a72a11ac2 | ||
|
|
66418bf591 | ||
|
|
7342385729 | ||
|
|
671911dc1d | ||
|
|
2ec55c6469 | ||
|
|
d678b3a304 | ||
|
|
bf3bd50624 | ||
|
|
a4f69daae7 | ||
|
|
127ee5508a | ||
|
|
d71fbf8c06 | ||
|
|
aa3e72ec6d | ||
|
|
9d7f4929ad | ||
|
|
319f667028 | ||
|
|
bcab1a0a4d | ||
|
|
11ea7dcb3b | ||
|
|
1bff81c6bb | ||
|
|
2016a208f2 | ||
|
|
dfc1b65e4b | ||
|
|
f7629eb1be | ||
|
|
72fc28c1fa | ||
|
|
da70d07051 | ||
|
|
c6db7e6479 | ||
|
|
82ad40ac82 | ||
|
|
4fd9b2ecf5 | ||
|
|
2dccf73050 | ||
|
|
d6532c60a6 | ||
|
|
99be8651aa | ||
|
|
8ca53bd765 | ||
|
|
26e49f6161 | ||
|
|
cca027b5eb | ||
|
|
51f9c09132 | ||
|
|
897204ead3 | ||
|
|
25735df0c6 | ||
|
|
e3b6183e00 | ||
|
|
b2741ad567 | ||
|
|
7905e97a84 | ||
|
|
b7d719132d | ||
|
|
210e03cea0 | ||
|
|
995fc59299 | ||
|
|
cbd8236cb4 | ||
|
|
095086cdfd | ||
|
|
59846a041f | ||
|
|
7d91d9e28b | ||
|
|
752f59b09b | ||
|
|
262c4eee93 | ||
|
|
031a6fdf3c | ||
|
|
ff208710d7 | ||
|
|
29f1041cc3 | ||
|
|
a613bfb45a | ||
|
|
ed49fd41db | ||
|
|
625f9b588e | ||
|
|
6fa73dce49 | ||
|
|
e8892af8ba | ||
|
|
e413991e63 | ||
|
|
e909ddd78b | ||
|
|
2ed5015aee | ||
|
|
6e89be2ac5 | ||
|
|
50fb4f47fd | ||
|
|
22f1d035b7 | ||
|
|
f654310c4d | ||
|
|
d44f7497a5 | ||
|
|
944ee74a1d | ||
|
|
ad964446de | ||
|
|
d41d9a3dab | ||
|
|
1a54a24fd5 | ||
|
|
ac51dfeab7 | ||
|
|
99a5aa1722 | ||
|
|
5791bdd5e7 | ||
|
|
f7514bc938 | ||
|
|
2bbaf7d1eb | ||
|
|
726e4de6ab |
No files matched your search
Regular → Executable
+21
-2
@@ -1,4 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Skip during merge/rebase/cherry-pick/revert. lint-staged's internal
|
||||
# git-stash mechanism is incompatible with these states and will silently
|
||||
# wipe MERGE_HEAD and your conflict resolutions on commit failure.
|
||||
git_dir=$(git rev-parse --git-dir)
|
||||
if [ -e "$git_dir/MERGE_HEAD" ] \
|
||||
|| [ -e "$git_dir/CHERRY_PICK_HEAD" ] \
|
||||
|| [ -e "$git_dir/REVERT_HEAD" ] \
|
||||
|| [ -d "$git_dir/rebase-merge" ] \
|
||||
|| [ -d "$git_dir/rebase-apply" ]; then
|
||||
echo "Skipping lint-staged (mid merge/rebase/cherry-pick/revert)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Anchor to repo root so commits made from a subdirectory still work, and
|
||||
# run each package's lint-staged in a subshell so cwd is scoped (a failure
|
||||
# inside one package can't poison the cwd for the next).
|
||||
cd "$(git rev-parse --show-toplevel)" || exit 1
|
||||
|
||||
echo "Running lint-staged..."
|
||||
cd client && npx lint-staged && cd ..
|
||||
cd server && npx lint-staged && cd ..
|
||||
(cd client && npx lint-staged) || exit 1
|
||||
(cd server && npx lint-staged) || exit 1
|
||||
@@ -12,20 +12,22 @@ Checkmate is an open-source uptime and infrastructure monitoring application. It
|
||||
```bash
|
||||
cd client
|
||||
npm install
|
||||
npm run dev # Start dev server at http://localhost:5173
|
||||
npm run dev -- --port 10001 --strictPort # Local dev port is 10001 (5173 is used by another project on this machine)
|
||||
npm run build # TypeScript check + production build
|
||||
npm run lint # ESLint (strict, max-warnings 0)
|
||||
npm run format # Prettier formatting
|
||||
npm run format-check # Check formatting
|
||||
```
|
||||
|
||||
Server `.env` on this machine is configured with `CLIENT_HOST="http://localhost:10001"` to match the client dev port. If you change the client port, keep `.env` in sync.
|
||||
|
||||
### Server (Node.js/Express)
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
|
||||
npm run build # TypeScript compile + path alias resolution
|
||||
npm run test # Run Mocha tests with c8 coverage
|
||||
npm run test # Run Jest tests with coverage
|
||||
npm run lint # ESLint v9
|
||||
npm run lint-fix # Auto-fix lint issues
|
||||
npm run format # Prettier formatting
|
||||
@@ -66,19 +68,23 @@ VITE_APP_LOG_LEVEL="debug"
|
||||
### Backend Layers
|
||||
```
|
||||
server/src/
|
||||
├── controllers/ # Route handlers (authController, monitorController, etc.)
|
||||
├── service/ # Business logic
|
||||
│ ├── business/ # Core monitoring logic
|
||||
│ ├── infrastructure/ # Server/system utilities
|
||||
│ └── system/ # App-level settings
|
||||
├── api/ # HTTP layer
|
||||
│ ├── controllers/ # Route handlers (authController, monitorController, etc.)
|
||||
│ ├── middleware/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
||||
│ ├── routes/ # API route definitions
|
||||
│ └── validation/ # Zod request-payload schemas
|
||||
├── domain/ # Business logic + data access, one folder per entity
|
||||
│ ├── monitors/ # e.g. monitor.service.ts, monitor.repository.ts, monitor.type.ts
|
||||
│ ├── checks/
|
||||
│ ├── incidents/
|
||||
│ └── … # users, teams, status-pages, notifications, etc.
|
||||
├── service/ # Cross-cutting services (e.g. infrastructure/)
|
||||
├── config/ # App config wiring (controllers, routes, services, envValidation)
|
||||
├── db/
|
||||
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
|
||||
│ ├── migration/ # Database migrations (run on startup)
|
||||
│ └── modules/ # Database-specific modules
|
||||
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
|
||||
├── routes/v1/ # API route definitions
|
||||
├── validation/ # Joi input validation schemas
|
||||
└── repositories/ # Data access layer
|
||||
│ └── migration/ # Database migrations (run on startup)
|
||||
├── templates/ # Email/notification templates
|
||||
├── types/ # Shared const tuples + derived types
|
||||
└── utils/ # Shared utilities
|
||||
```
|
||||
|
||||
### Frontend Structure
|
||||
@@ -157,6 +163,26 @@ When working on anything related to check scheduling, incident lifecycle, or not
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Coding conventions (mandatory)
|
||||
Read `docs/coding-conventions.md` before touching any `.tsx` or `.ts` file. The doc covers both frontend and backend rules, all enforced in code review.
|
||||
|
||||
**Universal — rule 0:** look at peer files first and follow the established pattern. Don't sneak in novel shapes.
|
||||
|
||||
**Frontend (`client/src`):**
|
||||
1. Prefer MUI native props over `sx` (e.g. `color={…}`, `bgcolor={…}`, `mt={…}` — not `sx={{ color, bgcolor, mt }}`).
|
||||
2. Use the full theme path for colors: `color={theme.palette.text.secondary}`, never `color="text.secondary"` (greppability).
|
||||
3. No hardcoded literals — use `LAYOUT.*`, `typographyLevels.*`, `theme.shape.borderRadius`, `theme.palette.*`.
|
||||
4. Use `useTheme()` inside components; don't `import { theme } from "@/Utils/Theme/Theme"`.
|
||||
5. Pair runtime tuples with derived types: `const X = [...] as const; type X = (typeof X)[number]`.
|
||||
|
||||
**Backend (`server/src`):**
|
||||
6. Layering: Controller → Service → Repository → Mongoose. No DB calls outside repositories.
|
||||
7. Provider conventions: DI for stdlib clients, `SERVICE_NAME` + `TIMEOUT_MS` constants, `timeRequest()` helper, inline `setTimeout` race for timeout, outer try/catch into `AppError`.
|
||||
8. Mongoose schema fields with closed value sets must declare `enum` reusing the same `types/*` const tuple.
|
||||
9. Provider tests at `server/test/unit/providers/network/<name>.test.ts`, `from "@jest/globals"`, `testStatusProviderContract`, inline `setup()` per test (not `beforeEach`).
|
||||
10. Centralize validation enums in `types/*` const tuples; never inline `z.enum([…])` more than once.
|
||||
11. New entity fields must land in the validator's `*ResponseSchema` so the auto-generated OpenAPI spec sees them.
|
||||
|
||||
### Internationalization
|
||||
All user-facing strings must use the translation function:
|
||||
```javascript
|
||||
@@ -174,12 +200,13 @@ t('your.key') // Never hardcode UI strings
|
||||
- Both use ESLint with strict settings
|
||||
|
||||
### Testing
|
||||
Server tests use Mocha + Chai + Sinon:
|
||||
Server tests use Jest (with `--experimental-vm-modules` for ESM):
|
||||
```bash
|
||||
npm test # Run all tests with coverage
|
||||
npm test -- --grep "pattern" # Run specific tests
|
||||
npm test # Run all tests with coverage
|
||||
npm test -- -t "pattern" # Run tests matching name pattern
|
||||
npm test -- path/to/file.test.ts # Run a specific file
|
||||
```
|
||||
Test files: `server/tests/**/*.test.js`
|
||||
Test files: `server/test/**/*.test.ts`
|
||||
|
||||
## Database Models
|
||||
|
||||
|
||||
+5
-18
@@ -151,6 +151,7 @@ LOG_LEVEL="debug"
|
||||
- `ORIGIN`: Origin for CORS purposes
|
||||
- `TOKEN_TTL`: Token time to live (in vercel/ms format)
|
||||
- `LOG_LEVEL`: Debug level (debug, info, warn, error)
|
||||
- `QUEUE_TYPE`: Job queue implementation (`superSimpleQueue` or `lessSimpleQueue`, optional, default: `superSimpleQueue`)
|
||||
|
||||
Start the backend server:
|
||||
|
||||
@@ -240,7 +241,7 @@ docker rm uptime_database_mongo
|
||||
|
||||
**Need more help?**
|
||||
|
||||
- Check the [full documentation](https://docs.checkmate.so)
|
||||
- Check the [full documentation](https://checkmate.so/docs)
|
||||
- Ask on [Discord](https://discord.com/invite/NAb6H3UTjK)
|
||||
|
||||
### Start contributing code?
|
||||
@@ -255,20 +256,6 @@ docker rm uptime_database_mongo
|
||||
|
||||
Start with [good first issues](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||
|
||||
### Improve the documentation?
|
||||
|
||||
Docs live in [checkmate-documentation](https://github.com/bluewave-labs/checkmate-documentation). You can fix typos, add guides, or explain features better.
|
||||
|
||||
### Help with translations?
|
||||
|
||||
We use [PoEditor](https://poeditor.com) for translations. You can:
|
||||
|
||||
- [Sign up and join your language team](https://poeditor.com/join/project/lRUoGZFCsJ).
|
||||
- Translate UI strings.
|
||||
- Ask questions on Discord in the relevant #translations channel.
|
||||
|
||||
Make sure all new UI strings in code use `t('key')`.
|
||||
|
||||
### Submit a pull request?
|
||||
|
||||
Follow the [pull request checklist](#pull-request-checklist). Your PR should:
|
||||
@@ -283,7 +270,7 @@ Follow the [pull request checklist](#pull-request-checklist). Your PR should:
|
||||
|
||||
## Code guidelines
|
||||
|
||||
- Use ESLint and Prettier (`npm run lint`).
|
||||
- Use ESLint and Prettier. Run `npm run lint` and `npm run format-check` in both `client` and `server`. If `format-check` reports issues, fix them with `npm run format` before committing.
|
||||
- Follow naming conventions: `camelCase` for variables, `PascalCase` for components, `UPPER_CASE` for constants.
|
||||
- No hard-coded strings — use `t('your.key')` for everything visible.
|
||||
- Use the shared theme and components. No magic numbers or hardcoded styles.
|
||||
@@ -304,7 +291,7 @@ Before submitting your pull request, please confirm the following:
|
||||
- You used the shared theme for any styling — no magic numbers or inline styles.
|
||||
- The pull request addresses only one issue or topic.
|
||||
- You added screenshots or a video for any UI-related changes.
|
||||
- Your code passes linting and has no TypeScript errors.
|
||||
- Your code passes `npm run lint`, `npm run format-check`, and `npm run build` in both `client` and `server` with no errors.
|
||||
|
||||
If one or more of these are missing, we may ask you to update your pull request before we can merge it.
|
||||
|
||||
@@ -316,7 +303,7 @@ If one or more of these are missing, we may ask you to update your pull request
|
||||
- `master` is used for stable releases.
|
||||
- Use descriptive branch names, like `fix/login-error` or `feat/add-alerts`.
|
||||
- Make sure that you are using the latest version.
|
||||
- Make sure you run the code locally. The Checkmate [documentation](https://docs.checkmate.so) covers it.
|
||||
- Make sure you run the code locally. The Checkmate [documentation](https://checkmate.so/docs) covers it.
|
||||
- Find out if the functionality is already covered, maybe by an individual configuration.
|
||||
- Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
||||
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
|
||||
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
<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/checkmate/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.
|
||||
@@ -1,5 +1,26 @@
|
||||
<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>
|
||||
|
||||
|
||||
<p align="center">
|
||||
🇺🇸 <a href="README.md">English</a> |
|
||||
🇸🇦 <a href="docs/translations/README.ar.md">العربية</a> |
|
||||
🇪🇸 <a href="docs/translations/README.ca.md">Català</a> |
|
||||
🇨🇿 <a href="docs/translations/README.cs.md">Čeština</a> |
|
||||
🇩🇪 <a href="docs/translations/README.de.md">Deutsch</a> |
|
||||
🇪🇸 <a href="docs/translations/README.es.md">Español</a> |
|
||||
🇫🇮 <a href="docs/translations/README.fi.md">Suomi</a> |
|
||||
🇫🇷 <a href="docs/translations/README.fr.md">Français</a> |
|
||||
🇮🇹 <a href="docs/translations/README.it.md">Italiano</a> |
|
||||
🇯🇵 <a href="docs/translations/README.ja.md">日本語</a> |
|
||||
🇧🇷 <a href="docs/translations/README.pt-BR.md">Português (Brasil)</a> |
|
||||
🇷🇺 <a href="docs/translations/README.ru.md">Русский</a> |
|
||||
🇹🇭 <a href="docs/translations/README.th.md">ไทย</a> |
|
||||
🇹🇷 <a href="docs/translations/README.tr.md">Türkçe</a> |
|
||||
🇺🇦 <a href="docs/translations/README.uk.md">Українська</a> |
|
||||
🇻🇳 <a href="docs/translations/README.vi.md">Tiếng Việt</a> |
|
||||
🇨🇳 <a href="docs/translations/README.zh-CN.md">简体中文</a> |
|
||||
🇹🇼 <a href="docs/translations/README.zh-TW.md">繁體中文</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
@@ -7,7 +28,6 @@
|
||||

|
||||

|
||||

|
||||
[](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>
|
||||
@@ -16,7 +36,10 @@
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -24,8 +47,6 @@ Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/c
|
||||
|
||||
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
|
||||
|
||||
**If you would like to sponsor a feature, [see this link](https://checkmate.so/sponsored-features).**
|
||||
|
||||
## 📚 Table of contents
|
||||
|
||||
- [📦 Demo](#demo)
|
||||
@@ -38,15 +59,16 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
||||
- [🏗️ Tech stack](#tech-stack)
|
||||
- [🔗 A few links](#a-few-links)
|
||||
- [🤝 Contributing](#contributing)
|
||||
- [💰 Our sponsors](#our-sponsors)
|
||||
|
||||
|
||||
<a id="demo"></a>
|
||||
## Demo
|
||||
|
||||
You can see the latest build of [Checkmate](https://demo.checkmate.so/) in action.
|
||||
|
||||
The username is demouser@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).
|
||||
|
||||
<a id="users-guide"></a>
|
||||
## User's guide
|
||||
|
||||
Usage instructions can be found [here](https://checkmate.so/docs).
|
||||
@@ -55,6 +77,7 @@ Usage instructions can be found [here](https://checkmate.so/docs).
|
||||
- [Docker](https://www.docker.com/) installed
|
||||
- [Git](https://git-scm.com/) installed
|
||||
|
||||
<a id="installation"></a>
|
||||
## Installation
|
||||
|
||||
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
||||
@@ -67,6 +90,7 @@ If you need to monitor internal HTTPS endpoints with certificates from private C
|
||||
|
||||
For more documentation, see the [docs directory](./docs/).
|
||||
|
||||
<a id="performance"></a>
|
||||
## 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:
|
||||
@@ -77,6 +101,7 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
||||
|
||||

|
||||
|
||||
<a id="questions--ideas"></a>
|
||||
## Questions & Ideas
|
||||
|
||||
If you have any questions, suggestions or comments, you have several options:
|
||||
@@ -86,33 +111,21 @@ If you have any questions, suggestions or comments, you have several options:
|
||||
|
||||
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
|
||||
<a id="features"></a>
|
||||
## Features
|
||||
|
||||
- Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5)
|
||||
- Website monitoring
|
||||
- Several monitoring options: Uptime, Docker, Ping, SSL, Port, Game server
|
||||
- Page speed monitoring
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
|
||||
- Selective disk monitoring with mountpoint selection
|
||||
- Docker monitoring
|
||||
- Ping monitoring
|
||||
- SSL monitoring
|
||||
- Port monitoring
|
||||
- Game server monitoring (3.0)
|
||||
- Incidents at a glance
|
||||
- Status pages
|
||||
- E-mail, Webhooks, Discord and Slack notifications
|
||||
- Status pages with 4 beautiful themes
|
||||
- E-mail, Webhooks, Discord, Slack, PagerDuty, Matrix, Microsoft Teams, Telegram, Pushover, Twilio (SMS) notifications
|
||||
- Scheduled maintenance
|
||||
- JSON query monitoring
|
||||
- Multi-language support for English, German, Japanese, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, Chinese (Traditional, Taiwan)
|
||||
- Multi-language support for Arabic, Chinese (Simplified), Chinese (Traditional, Taiwan), Czech, English, Finnish, French, German, Japanese, Portuguese (Brazil), Russian, Spanish, Thai, Turkish, Ukrainian, and Vietnamese
|
||||
|
||||
**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
|
||||
|
||||
If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features).
|
||||
|
||||
## Monitor Lifecycle
|
||||
|
||||
@@ -123,6 +136,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
|
||||
5. Upon a state change: an incident is either created or resolved, depending on the monitor's current status
|
||||
6. Notifications are triggered based on configuration
|
||||
|
||||
<a id="screenshots"></a>
|
||||
## Screenshots
|
||||
|
||||
<p>
|
||||
@@ -140,6 +154,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
|
||||
|
||||
|
||||
|
||||
<a id="tech-stack"></a>
|
||||
## Tech stack
|
||||
|
||||
- [ReactJs](https://react.dev/)
|
||||
@@ -149,6 +164,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
|
||||
- [Recharts](https://recharts.org)
|
||||
- Lots of other open source components!
|
||||
|
||||
<a id="a-few-links"></a>
|
||||
## A few links
|
||||
|
||||
- If you would like to support us, please consider giving it a ⭐ and click on "watch".
|
||||
@@ -156,6 +172,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec
|
||||
- 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)
|
||||
|
||||
<a id="contributing"></a>
|
||||
## Contributing
|
||||
|
||||
We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [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.
|
||||
@@ -180,8 +197,3 @@ Here's how you can contribute:
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/Checkmate&Date)
|
||||
|
||||
## Our sponsors
|
||||
|
||||
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
|
||||
|
||||
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
|
||||
@@ -2,5 +2,5 @@ apiVersion: v2
|
||||
name: checkmate-chart
|
||||
description: A Helm chart for Checkmate App
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "2.3"
|
||||
version: 0.2.0
|
||||
appVersion: "v3.8.1" # "v" prefix: published image tags are v-prefixed (ghcr.io/.../checkmate-*:v3.8.1)
|
||||
@@ -19,12 +19,11 @@ cd checkmate/charts/helm/checkmate
|
||||
|
||||
### 2. Customize values.yaml
|
||||
Edit `values.yaml` to update:
|
||||
- `client.ingress.host` and `server.ingress.host` with your domain names
|
||||
- `server.protocol` (usually http or https)
|
||||
- `client.ingress.host` and `api.ingress.host` with your domain names
|
||||
- `api.protocol` (usually http or https)
|
||||
- **If upgrading**: Migrate persistence settings from flat structure to nested:
|
||||
- Old: `persistence.mongodbSize` → New: `persistence.mongo.size`
|
||||
- Old: `persistence.redisSize` → New: `persistence.redis.size`
|
||||
- Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default)
|
||||
- Add: `persistence.mongo.storageClass` (leave empty for default)
|
||||
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
|
||||
- **For TLS/HTTPS**: Configure ingress TLS settings (see section below)
|
||||
|
||||
@@ -32,7 +31,7 @@ Edit `values.yaml` to update:
|
||||
```bash
|
||||
helm install checkmate ./charts/helm/checkmate
|
||||
```
|
||||
This will deploy the client, server, MongoDB, and Redis components.
|
||||
This will deploy the client, api, and MongoDB components.
|
||||
|
||||
### 4. Verify the deployment
|
||||
Check pods and services:
|
||||
@@ -43,6 +42,137 @@ kubectl get svc
|
||||
|
||||
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
|
||||
|
||||
## Upgrading from a previous release
|
||||
|
||||
This release renames the `server.*` value block to `api.*` and drops the (unused) bundled Redis.
|
||||
Read this before running `helm upgrade` on an existing install.
|
||||
|
||||
### `server.*` → `api.*` (values rename)
|
||||
|
||||
The value block formerly named `server` is now `api` (`server.image` → `api.image`,
|
||||
`server.ingress.host` → `api.ingress.host`, `server.protocol` → `api.protocol`, `server.resources`,
|
||||
`server.affinity`, `server.tolerations`, `server.ingress.*`, etc.).
|
||||
|
||||
**Your existing `server.*` overrides keep working** — the chart merges any legacy `server.*` values on
|
||||
top of the `api.*` defaults (legacy wins where set), so an unchanged values file upgrades cleanly. The
|
||||
shim is transitional; migrate your overrides to `api.*` at your convenience and drop the `server:`
|
||||
block. If you set **both** `server.*` and `api.*` for the same field, the legacy `server.*` value wins —
|
||||
don't mix them.
|
||||
|
||||
### Resource renames cause a replace, not a rename
|
||||
|
||||
The Kubernetes objects are renamed alongside the values:
|
||||
|
||||
| Old object | New object |
|
||||
| --- | --- |
|
||||
| Deployment/Service `checkmate-server` | `checkmate-api` |
|
||||
| Ingress `checkmate-server-ingress` | `checkmate-api-ingress` |
|
||||
| ConfigMap `checkmate-server-nginx-cm` | `checkmate-client-nginx-cm` |
|
||||
|
||||
Helm deletes the old objects and creates the new ones on upgrade. Expect a brief API restart and a
|
||||
**new ClusterIP** for the API Service — update anything outside the chart that referenced the
|
||||
`checkmate-server` Service by name. MongoDB data (its PVC) is untouched.
|
||||
|
||||
### Redis removed
|
||||
|
||||
The bundled Redis StatefulSet/Service and the `redis.*` / `persistence.redis.*` values are gone — the
|
||||
scheduler runs in-process and never used them. Default installs (`redis.enabled: false`) are
|
||||
unaffected. If you had explicitly set `redis.enabled: true`, **the Redis StatefulSet and its PVC are
|
||||
deleted on upgrade** — back up anything you stored there first. Leftover `redis:` / `persistence.redis:`
|
||||
keys in your values file are harmless (ignored).
|
||||
|
||||
### New secret defaults
|
||||
|
||||
`NODE_ENV: production`, `LOG_LEVEL: info`, and `TOKEN_TTL: 99d` are now part of the default `secrets`
|
||||
block and merge into your install unless you override them. Set them explicitly if you relied on
|
||||
different values (e.g. a non-production `NODE_ENV`).
|
||||
|
||||
### Image tags follow the chart's `appVersion`
|
||||
|
||||
Each tier's `image` value is now the repository **without a tag**; the tag defaults to the chart's
|
||||
`appVersion`, so the client, api, worker, and mongodb images all move together when you upgrade the
|
||||
chart. You can still pin per tier:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend # repo only
|
||||
tag: "" # empty → Chart.appVersion; set e.g. "v3.1.0" to pin, or "latest" for a dev tag
|
||||
```
|
||||
|
||||
- `<tier>.tag` overrides the version for that tier. The `worker` tier inherits `api.tag` (and
|
||||
`api.image`) unless you set `worker.tag` / `worker.image`.
|
||||
- If you put a **full `repo:tag`** in `image` (e.g. a legacy `server.image: …:v3.2.0` override), it is
|
||||
used verbatim and the `tag` field is ignored — so existing pinned values keep working unchanged.
|
||||
|
||||
**Prefer a pinned tag over `latest`.** With the default `imagePullPolicy: IfNotPresent`, `latest` is
|
||||
cached per node and never refreshed, and `helm rollback` can't move a floating tag — pin a version (or
|
||||
let it default to `appVersion`) for reproducible, rollback-able deploys.
|
||||
|
||||
## Scaling: the worker tier & autoscaling
|
||||
|
||||
By default the chart runs a single all-in-one API pod that both schedules **and** processes monitoring
|
||||
jobs (`worker.enabled: false`). For larger deployments you can split processing onto a dedicated,
|
||||
horizontally-scalable worker tier.
|
||||
|
||||
### Topology
|
||||
|
||||
| `worker.enabled` | API (`checkmate-api`) | Worker (`checkmate-worker`) |
|
||||
| --- | --- | --- |
|
||||
| `false` (default) | schedules **and** processes jobs (`QUEUE_PRIMARY_PROCESSES=true`) | not deployed |
|
||||
| `true` | schedules + serves the API only (`QUEUE_PRIMARY_PROCESSES=false`, derived) | processes all jobs |
|
||||
|
||||
`QUEUE_PRIMARY_PROCESSES` is **derived** from `worker.enabled` — you never set it directly, so the API
|
||||
and worker tier can never both process (or both ignore) the queue.
|
||||
|
||||
### Enabling the worker tier
|
||||
|
||||
```yaml
|
||||
worker:
|
||||
enabled: true
|
||||
replicas: 2 # used only when autoscaling.enabled = false
|
||||
```
|
||||
|
||||
The worker takes **no inbound traffic** (no Service): the kubelet probes pod IPs directly on
|
||||
`HEALTH_PORT` (`/livez`, `/readyz`), and graceful shutdown drains in-flight jobs before exit.
|
||||
|
||||
> **Invariant:** `worker.terminationGracePeriodSeconds` (default `30`) **must stay greater than the
|
||||
> server drain timeout (25s)**. Lower it and Kubernetes SIGKILLs workers mid-drain, losing in-flight
|
||||
> checks.
|
||||
|
||||
> **Caveat:** with the worker tier enabled the API no longer processes jobs, so if **all** workers are
|
||||
> down nothing processes. The `autoscaling.minReplicaCount: 1` floor plus liveness restarts mitigate
|
||||
> this; full failover to the API would need leader election (not supported).
|
||||
|
||||
### Autoscaling with KEDA
|
||||
|
||||
Backlog-driven autoscaling is **off by default** and requires the
|
||||
[KEDA operator](https://keda.sh/) installed in-cluster. Enable it with:
|
||||
|
||||
```yaml
|
||||
worker:
|
||||
enabled: true
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicaCount: 1 # processing floor — never 0, or checks silently stall
|
||||
maxReplicaCount: 10
|
||||
backlogPerReplica: 50 # target due-job backlog per worker
|
||||
mongo:
|
||||
dbName: uptime_db # MUST match the database in DB_CONNECTION_STRING
|
||||
```
|
||||
|
||||
A KEDA `ScaledObject` queries MongoDB directly for the due-check backlog and scales the worker
|
||||
Deployment between `minReplicaCount` and `maxReplicaCount`. When autoscaling is on, the Deployment
|
||||
omits `replicas` so it doesn't fight the HPA KEDA creates. Make sure `autoscaling.mongo.dbName`
|
||||
matches the database name in your `DB_CONNECTION_STRING`, or the scaler counts the wrong DB (always 0
|
||||
backlog → never scales up).
|
||||
|
||||
#### Fallback: Prometheus-adapter HPA (not shipped)
|
||||
|
||||
If your KEDA/Mongo versions reject the `$$NOW`/`$toLong` query, or you already run Prometheus, scrape
|
||||
the worker's `/metrics` (`checkmate_worker_due_backlog`), expose it via prometheus-adapter, and target
|
||||
it with a standard `HorizontalPodAutoscaler`. The manifest is left to the operator; the app computes
|
||||
`now` in code on this path, so the query coercion is unnecessary.
|
||||
|
||||
## Enabling TLS/HTTPS with cert-manager
|
||||
|
||||
If you have [cert-manager](https://cert-manager.io/) installed in your cluster, you can enable automatic TLS certificate provisioning using Let's Encrypt or other certificate issuers.
|
||||
@@ -68,7 +198,7 @@ client:
|
||||
enabled: true
|
||||
secretName: checkmate-client-tls
|
||||
|
||||
server:
|
||||
api:
|
||||
protocol: https
|
||||
ingress:
|
||||
enabled: true
|
||||
@@ -78,7 +208,7 @@ server:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: checkmate-server-tls
|
||||
secretName: checkmate-api-tls
|
||||
```
|
||||
|
||||
### Alternative: Using --set flags
|
||||
@@ -88,13 +218,13 @@ You can also enable TLS during installation using Helm's `--set` flags:
|
||||
```bash
|
||||
helm install checkmate ./charts/helm/checkmate \
|
||||
--set client.protocol=https \
|
||||
--set server.protocol=https \
|
||||
--set api.protocol=https \
|
||||
--set client.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
||||
--set client.ingress.tls.enabled=true \
|
||||
--set client.ingress.tls.secretName=checkmate-client-tls \
|
||||
--set server.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
||||
--set server.ingress.tls.enabled=true \
|
||||
--set server.ingress.tls.secretName=checkmate-server-tls
|
||||
--set api.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
|
||||
--set api.ingress.tls.enabled=true \
|
||||
--set api.ingress.tls.secretName=checkmate-api-tls
|
||||
```
|
||||
|
||||
### Verification
|
||||
@@ -107,7 +237,7 @@ kubectl get certificates
|
||||
|
||||
# Check certificate details
|
||||
kubectl describe certificate checkmate-client-tls
|
||||
kubectl describe certificate checkmate-server-tls
|
||||
kubectl describe certificate checkmate-api-tls
|
||||
|
||||
# Verify the secrets were created
|
||||
kubectl get secrets | grep checkmate-tls
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{{- /*
|
||||
Back-compat shim: the `server.*` value block was renamed to `api.*`. Existing installs that
|
||||
still pass a legacy `server:` block keep working — this returns the effective API values with any
|
||||
legacy `server.*` overrides merged on top of the `api.*` defaults (legacy wins where set).
|
||||
|
||||
New installs have no `server:` key, so this is a no-op deepCopy of `.Values.api`. Consume it as:
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
then reference `$api.*` instead of `.Values.api.*`.
|
||||
*/}}
|
||||
{{- define "checkmate.api" -}}
|
||||
{{- $api := deepCopy .Values.api -}}
|
||||
{{- with .Values.server -}}
|
||||
{{- $api = mergeOverwrite $api (deepCopy .) -}}
|
||||
{{- end -}}
|
||||
{{- $api | toYaml -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Resolve a full image reference from a repo + optional tag. `appVersion` is the single source of
|
||||
truth for tags: bumping Chart.appVersion moves every tier at once. Call as:
|
||||
{{ include "checkmate.image" (dict "image" $repo "tag" $tag "root" $) }}
|
||||
|
||||
If `image` already carries a tag (a ":" in its final path segment) it is used verbatim — this keeps
|
||||
legacy `server.image: repo:tag` overrides working and lets anyone pin a full ref (e.g. tag "latest").
|
||||
A ":" only in the registry segment (a registry port like "reg:5000/x") is ignored, so a tag is still
|
||||
appended. Otherwise the tag is `tag` if set, else the chart's appVersion.
|
||||
*/}}
|
||||
{{- define "checkmate.image" -}}
|
||||
{{- $lastSegment := .image | splitList "/" | last -}}
|
||||
{{- if contains ":" $lastSegment -}}
|
||||
{{- .image -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s:%s" .image (.tag | default .root.Chart.AppVersion) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,65 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: checkmate-api
|
||||
spec:
|
||||
replicas: 1 # single scheduler — seeds/reconciles the jobs collection
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkmate-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkmate-api
|
||||
spec:
|
||||
{{- with $api.affinity }}
|
||||
affinity:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $api.tolerations }}
|
||||
tolerations:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: api
|
||||
image: {{ include "checkmate.image" (dict "image" $api.image "tag" $api.tag "root" $) }}
|
||||
imagePullPolicy: {{ $api.imagePullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ $api.port }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets
|
||||
# Probes hit the Express /api/v1/health endpoint — proves the HTTP server is responsive.
|
||||
# NOTE: this does NOT detect a wedged job loop when queuePrimaryProcesses=true (the API is
|
||||
# also a worker). Worker-aware health for that mode is a follow-up — see worker-health-checks.md §7.5.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health
|
||||
port: http
|
||||
initialDelaySeconds: {{ $api.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $api.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ $api.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ $api.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/v1/health
|
||||
port: http
|
||||
initialDelaySeconds: {{ $api.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ $api.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ $api.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ $api.probes.readiness.failureThreshold }}
|
||||
{{- with $api.resources }}
|
||||
resources:
|
||||
{{- tpl ( . | toYaml) $ | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
- name: CLIENT_HOST
|
||||
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
|
||||
- name: QUEUE_MODE
|
||||
value: "primary"
|
||||
# Derived: when a worker tier is deployed the API only schedules (false); otherwise it is
|
||||
# all-in-one and processes jobs itself (true). Keeps the two from ever competing for jobs.
|
||||
- name: QUEUE_PRIMARY_PROCESSES
|
||||
value: {{ not .Values.worker.enabled | quote }}
|
||||
+12
-11
@@ -1,11 +1,12 @@
|
||||
{{- if .Values.server.ingress.enabled }}
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
{{- if $api.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: checkmate-server-ingress
|
||||
{{- if .Values.server.ingress.annotations }}
|
||||
name: checkmate-api-ingress
|
||||
{{- if $api.ingress.annotations }}
|
||||
annotations:
|
||||
{{- range $key, $value := .Values.server.ingress.annotations }}
|
||||
{{- range $key, $value := $api.ingress.annotations }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -17,22 +18,22 @@ metadata:
|
||||
#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:
|
||||
ingressClassName: {{ .Values.server.ingress.className }}
|
||||
{{- if .Values.server.ingress.tls.enabled }}
|
||||
ingressClassName: {{ $api.ingress.className }}
|
||||
{{- if $api.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .Values.server.ingress.host }}
|
||||
secretName: {{ default (printf "%s-server-tls" .Release.Name) .Values.server.ingress.tls.secretName }}
|
||||
- {{ $api.ingress.host }}
|
||||
secretName: {{ default (printf "%s-api-tls" .Release.Name) $api.ingress.tls.secretName }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.server.ingress.host }}
|
||||
- host: {{ $api.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /api/v1
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: checkmate-server
|
||||
name: checkmate-api
|
||||
port:
|
||||
number: {{ .Values.server.port }}
|
||||
number: {{ $api.port }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: checkmate-api
|
||||
spec:
|
||||
selector:
|
||||
app: checkmate-api
|
||||
ports:
|
||||
- port: {{ $api.port }}
|
||||
+5
-3
@@ -1,3 +1,4 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -22,12 +23,13 @@ spec:
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: client
|
||||
image: {{ .Values.client.image }}
|
||||
image: {{ include "checkmate.image" (dict "image" .Values.client.image "tag" .Values.client.tag "root" $) }}
|
||||
imagePullPolicy: {{ .Values.client.imagePullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.client.port }}
|
||||
env:
|
||||
- name: UPTIME_APP_API_BASE_URL
|
||||
value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1"
|
||||
value: "{{ $api.protocol }}://{{ $api.ingress.host }}/api/v1"
|
||||
- name: UPTIME_APP_CLIENT_HOST
|
||||
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
|
||||
volumeMounts:
|
||||
@@ -41,4 +43,4 @@ spec:
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: checkmate-server-nginx-cm
|
||||
name: checkmate-client-nginx-cm
|
||||
File renamed without changes.
+5
-4
@@ -1,7 +1,8 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: checkmate-server-nginx-cm
|
||||
name: checkmate-client-nginx-cm
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
@@ -22,7 +23,7 @@ data:
|
||||
}
|
||||
|
||||
# location /api/ {
|
||||
# proxy_pass http://{{ .Values.server.ingress.host }}:5000/api/;
|
||||
# proxy_pass http://{{ $api.ingress.host }}:5000/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -31,7 +32,7 @@ data:
|
||||
# }
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://{{ .Values.server.ingress.host }}:5000/api-docs/;
|
||||
proxy_pass http://{{ $api.ingress.host }}:5000/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
File renamed without changes.
File renamed without changes.
+5
-2
@@ -22,7 +22,8 @@ spec:
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: mongodb
|
||||
image: {{ .Values.mongodb.image }}
|
||||
image: {{ include "checkmate.image" (dict "image" .Values.mongodb.image "tag" .Values.mongodb.tag "root" $) }}
|
||||
imagePullPolicy: {{ .Values.mongodb.imagePullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.mongodb.port }}
|
||||
command: ["mongod", "--quiet", "--bind_ip_all"]
|
||||
@@ -37,7 +38,9 @@ spec:
|
||||
- metadata:
|
||||
name: checkmate-mongo-persistent-storage
|
||||
spec:
|
||||
storageClassName: {{ .Values.persistence.mongo.storageClass | quote }}
|
||||
{{- with .Values.persistence.mongo.storageClass }}
|
||||
storageClassName: {{ . | quote }} {{/* omitted when empty so the cluster's default StorageClass applies */}}
|
||||
{{- end }}
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
@@ -1,14 +1,15 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
{{- if eq .Values.client.ingress.host "change_me" }}
|
||||
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.server.ingress.host "change_me" }}
|
||||
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- if eq $api.ingress.host "change_me" }}
|
||||
{{- fail "api.ingress.host (or legacy server.ingress.host) must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $serverProtocol := .Values.server.protocol }}
|
||||
{{- if not (or (eq $serverProtocol "http") (eq $serverProtocol "https")) }}
|
||||
{{- fail "server.protocol must be either 'http' or 'https'" }}
|
||||
{{- $apiProtocol := $api.protocol }}
|
||||
{{- if not (or (eq $apiProtocol "http") (eq $apiProtocol "https")) }}
|
||||
{{- fail "api.protocol must be either 'http' or 'https'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $clientProtocol := .Values.client.protocol }}
|
||||
@@ -21,28 +22,33 @@
|
||||
{{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.server.ingress.tls.enabled (ne $serverProtocol "https") }}
|
||||
{{- fail "server.ingress.tls.enabled is true but server.protocol is not 'https'. Set server.protocol: https to ensure correct API base URL." }}
|
||||
{{- if and $api.ingress.tls.enabled (ne $apiProtocol "https") }}
|
||||
{{- fail "api.ingress.tls.enabled is true but api.protocol is not 'https'. Set api.protocol: https to ensure correct API base URL." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* If client runs on https, API must also be https to avoid mixed content */}}
|
||||
{{- if and (eq $clientProtocol "https") (ne $serverProtocol "https") }}
|
||||
{{- fail "client.protocol is 'https' but server.protocol is not. Set server.protocol: https to prevent browser mixed-content issues." }}
|
||||
{{- if and (eq $clientProtocol "https") (ne $apiProtocol "https") }}
|
||||
{{- fail "client.protocol is 'https' but api.protocol is not. Set api.protocol: https to prevent browser mixed-content issues." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Fail early if TLS enabled without cert-manager annotations (cluster-issuer or issuer) */}}
|
||||
{{- $cAnn := .Values.client.ingress.annotations | default dict }}
|
||||
{{- $sAnn := .Values.server.ingress.annotations | default dict }}
|
||||
{{- $aAnn := $api.ingress.annotations | default dict }}
|
||||
|
||||
{{- $clientHasIssuer := or (hasKey $cAnn "cert-manager.io/cluster-issuer") (hasKey $cAnn "cert-manager.io/issuer") }}
|
||||
{{- $serverHasIssuer := or (hasKey $sAnn "cert-manager.io/cluster-issuer") (hasKey $sAnn "cert-manager.io/issuer") }}
|
||||
{{- $apiHasIssuer := or (hasKey $aAnn "cert-manager.io/cluster-issuer") (hasKey $aAnn "cert-manager.io/issuer") }}
|
||||
|
||||
{{- if and .Values.client.ingress.tls.enabled (not $clientHasIssuer) }}
|
||||
{{- fail "client.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.server.ingress.tls.enabled (not $serverHasIssuer) }}
|
||||
{{- fail "server.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
||||
{{- if and $api.ingress.tls.enabled (not $apiHasIssuer) }}
|
||||
{{- fail "api.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Secret name can be omitted; we default to <release>-client|server-tls in templates */}}
|
||||
{{/* Worker grace period must exceed the server drain timeout (25s) or workers are SIGKILLed mid-drain */}}
|
||||
{{- if and .Values.worker.enabled (le (int .Values.worker.terminationGracePeriodSeconds) 25) }}
|
||||
{{- fail "worker.terminationGracePeriodSeconds must be greater than 25 (the server drain timeout) or workers are killed mid-drain" }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Secret name can be omitted; we default to <release>-client|api-tls in templates */}}
|
||||
@@ -1,11 +0,0 @@
|
||||
{{- if .Values.redis.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: checkmate-redis
|
||||
spec:
|
||||
selector:
|
||||
app: checkmate-redis
|
||||
ports:
|
||||
- port: {{ .Values.redis.port }}
|
||||
{{- end }}
|
||||
@@ -1,33 +0,0 @@
|
||||
{{- if .Values.redis.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: checkmate-redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkmate-redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkmate-redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: {{ .Values.redis.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.redis.port }}
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: checkmate-redis-persistent-storage
|
||||
spec:
|
||||
storageClassName: {{ .Values.persistence.redis.storageClass | quote }}
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.redis.size | default "1Gi" | quote }}
|
||||
{{- end }}
|
||||
@@ -4,6 +4,14 @@
|
||||
{{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- /*
|
||||
The bundled Mongo service ("checkmate-mongodb") lives in .Release.Namespace. App pods reach it
|
||||
by bare name, but the KEDA operator runs in its own namespace and a bare name is NXDOMAIN there,
|
||||
so worker autoscaling fails to query the backlog. Qualify the in-cluster host to its FQDN here so
|
||||
the one connection string resolves from every namespace. External/already-qualified hosts are not
|
||||
matched (the char after the host name is "." for an FQDN, which the pattern excludes).
|
||||
*/}}
|
||||
{{- $mongoFqdn := printf "checkmate-mongodb.%s.svc.cluster.local" .Release.Namespace }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -11,5 +19,9 @@ metadata:
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- range $key, $value := $secrets }}
|
||||
{{- if eq $key "DB_CONNECTION_STRING" }}
|
||||
{{ $key }}: {{ regexReplaceAll "(//|@)checkmate-mongodb([:/,]|$)" $value (printf "${1}%s${2}" $mongoFqdn) | quote }}
|
||||
{{- else }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,37 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: checkmate-server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkmate-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkmate-server
|
||||
spec:
|
||||
{{- with .Values.server.affinity }}
|
||||
affinity:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.tolerations }}
|
||||
tolerations:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: server
|
||||
image: {{ .Values.server.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.server.port }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets
|
||||
{{- with .Values.server.resources }}
|
||||
resources:
|
||||
{{- tpl ( . | toYaml) $ | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
- name: CLIENT_HOST
|
||||
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: checkmate-server
|
||||
spec:
|
||||
selector:
|
||||
app: checkmate-server
|
||||
ports:
|
||||
- port: {{ .Values.server.port }}
|
||||
@@ -0,0 +1,66 @@
|
||||
{{- $api := include "checkmate.api" . | fromYaml -}}
|
||||
{{- if .Values.worker.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: checkmate-worker
|
||||
spec:
|
||||
{{- if not .Values.worker.autoscaling.enabled }}
|
||||
replicas: {{ .Values.worker.replicas }} # omitted when autoscaling owns the replica count
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkmate-worker
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkmate-worker
|
||||
spec:
|
||||
terminationGracePeriodSeconds: {{ .Values.worker.terminationGracePeriodSeconds }}
|
||||
{{- with .Values.worker.affinity }}
|
||||
affinity:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.worker.tolerations }}
|
||||
tolerations:
|
||||
{{- tpl ( . | toYaml) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: worker
|
||||
image: {{ include "checkmate.image" (dict "image" (.Values.worker.image | default $api.image) "tag" (.Values.worker.tag | default $api.tag) "root" $) }}
|
||||
imagePullPolicy: {{ .Values.worker.imagePullPolicy | default $api.imagePullPolicy }}
|
||||
ports:
|
||||
- name: health
|
||||
containerPort: {{ .Values.worker.healthPort }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets # DB_CONNECTION_STRING (+ JWT_SECRET/CLIENT_HOST for validateEnv)
|
||||
env:
|
||||
# Secret default CLIENT_HOST is the non-URL "change_me"; override like the API so validateEnv passes.
|
||||
- name: CLIENT_HOST
|
||||
value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}"
|
||||
- name: QUEUE_MODE
|
||||
value: "worker"
|
||||
- name: HEALTH_PORT
|
||||
value: {{ .Values.worker.healthPort | quote }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /livez
|
||||
port: health
|
||||
initialDelaySeconds: {{ .Values.worker.probes.liveness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.worker.probes.liveness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.worker.probes.liveness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.worker.probes.liveness.failureThreshold }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: health
|
||||
initialDelaySeconds: {{ .Values.worker.probes.readiness.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.worker.probes.readiness.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.worker.probes.readiness.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.worker.probes.readiness.failureThreshold }}
|
||||
{{- with .Values.worker.resources }}
|
||||
resources:
|
||||
{{- tpl ( . | toYaml) $ | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{- if and .Values.worker.enabled .Values.worker.autoscaling.enabled }}
|
||||
apiVersion: keda.sh/v1alpha1
|
||||
kind: ScaledObject
|
||||
metadata:
|
||||
name: checkmate-worker
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
name: checkmate-worker
|
||||
minReplicaCount: {{ .Values.worker.autoscaling.minReplicaCount }} # floor: never 0 — a cold gap stops checks
|
||||
maxReplicaCount: {{ .Values.worker.autoscaling.maxReplicaCount }}
|
||||
cooldownPeriod: {{ .Values.worker.autoscaling.cooldownPeriod }}
|
||||
advanced:
|
||||
horizontalPodAutoscalerConfig:
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: {{ .Values.worker.autoscaling.stabilizationWindowSeconds }}
|
||||
triggers:
|
||||
- type: mongodb
|
||||
metadata:
|
||||
dbName: {{ .Values.worker.autoscaling.mongo.dbName | quote }}
|
||||
collection: {{ .Values.worker.autoscaling.mongo.collection | quote }}
|
||||
# The Phase 2 countDueBacklog predicate, time-relativized with $$NOW (epoch-ms via $toLong):
|
||||
query: >-
|
||||
{ "type": "check", "isActive": true,
|
||||
"$expr": { "$lte": ["$nextScheduledAt", { "$toLong": "$$NOW" }] },
|
||||
"$or": [ { "lockedUntil": null },
|
||||
{ "$expr": { "$lt": ["$lockedUntil", { "$toLong": "$$NOW" }] } } ] }
|
||||
queryValue: {{ .Values.worker.autoscaling.backlogPerReplica | quote }}
|
||||
connectionStringFromEnv: DB_CONNECTION_STRING # resolved from the worker's checkmate-secrets
|
||||
{{- end }}
|
||||
@@ -1,5 +1,7 @@
|
||||
client:
|
||||
image: ghcr.io/bluewave-labs/checkmate-client:v3.2.0
|
||||
image: ghcr.io/bluewave-labs/checkmate-client
|
||||
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin (e.g. a :latest dev tag)
|
||||
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
|
||||
port: 80
|
||||
protocol: http
|
||||
ingress:
|
||||
@@ -17,10 +19,24 @@ client:
|
||||
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
|
||||
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
|
||||
|
||||
server:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
|
||||
api:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend
|
||||
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin (e.g. a :latest dev tag)
|
||||
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
|
||||
port: 52345
|
||||
protocol: http
|
||||
# Liveness/readiness probes hit GET /api/v1/health on the api port (proves the HTTP server is up).
|
||||
probes:
|
||||
liveness:
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness:
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
ingress:
|
||||
enabled: true
|
||||
host: change_me
|
||||
@@ -31,40 +47,62 @@ server:
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
enabled: false
|
||||
# secretName: {{ .Release.Name }}-server-tls # Optional; defaults to <release>-server-tls if omitted
|
||||
# Note: when enabling TLS, also set server.protocol: https and add
|
||||
# secretName: {{ .Release.Name }}-api-tls # Optional; defaults to <release>-api-tls if omitted
|
||||
# Note: when enabling TLS, also set api.protocol: https and add
|
||||
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
|
||||
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
|
||||
|
||||
redis:
|
||||
enabled: false
|
||||
image: redis:7.2
|
||||
port: 6379
|
||||
|
||||
mongodb:
|
||||
image: ghcr.io/bluewave-labs/checkmate-mongo:v3.2.0
|
||||
image: ghcr.io/bluewave-labs/checkmate-mongo
|
||||
tag: "" # defaults to Chart.appVersion; set a full repo:tag in `image` to pin
|
||||
imagePullPolicy: IfNotPresent # IfNotPresent so kind-loaded :latest images are used (not pulled)
|
||||
port: 27017
|
||||
|
||||
worker:
|
||||
# Off by default; existing installs are unchanged. When true, a dedicated worker processes all
|
||||
# jobs and the API stops processing (QUEUE_PRIMARY_PROCESSES is derived as false). When false, the
|
||||
# API is all-in-one and processes jobs itself.
|
||||
enabled: false
|
||||
image: "" # defaults to api.image when empty, same as API image
|
||||
tag: "" # defaults to api.tag (-> Chart.appVersion) when empty
|
||||
imagePullPolicy: "" # defaults to api.imagePullPolicy when empty
|
||||
replicas: 1 # used only when autoscaling.enabled = false
|
||||
healthPort: 52346 # HEALTH_PORT; serves /livez /readyz /metrics
|
||||
terminationGracePeriodSeconds: 30 # MUST stay > the server drain timeout (25s) or workers are killed mid-drain
|
||||
resources: {}
|
||||
affinity: {}
|
||||
tolerations: {}
|
||||
probes:
|
||||
liveness: # /livez — failureThreshold>=3 so one slow tick never restarts mid-job
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readiness: # /readyz — flips to 503 on drain
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
autoscaling:
|
||||
enabled: false # requires the KEDA operator installed in-cluster
|
||||
minReplicaCount: 1 # processing floor — never 0 (a cold gap silently stops checks)
|
||||
maxReplicaCount: 10
|
||||
backlogPerReplica: 50 # KEDA queryValue: target due-backlog per worker
|
||||
cooldownPeriod: 300 # seconds before scaling an idle target down
|
||||
stabilizationWindowSeconds: 300 # damp the interval-boundary herd
|
||||
mongo:
|
||||
dbName: uptime_db # must match DB_CONNECTION_STRING's database
|
||||
collection: jobs
|
||||
|
||||
secrets:
|
||||
JWT_SECRET: change_me
|
||||
# REFRESH_TOKEN_SECRET: change_me
|
||||
# SYSTEM_EMAIL_ADDRESS: test@example.com
|
||||
# SYSTEM_EMAIL_PASSWORD: change_me
|
||||
# SYSTEM_EMAIL_HOST: smtp.example.com
|
||||
# SYSTEM_EMAIL_PORT: "587"
|
||||
# PAGESPEED_API_KEY: change_me
|
||||
DB_CONNECTION_STRING: mongodb://checkmate-mongodb.namespace.svc:27017/uptime_db
|
||||
DB_CONNECTION_STRING: mongodb://checkmate-mongodb:27017/uptime_db
|
||||
CLIENT_HOST: change_me
|
||||
# REDIS_HOST: redis
|
||||
# REDIS_PORT: "6379"
|
||||
# DB_TYPE: MongoDB
|
||||
# TOKEN_TTL: 99d
|
||||
# REFRESH_TOKEN_TTL: 99d
|
||||
NODE_ENV: production
|
||||
LOG_LEVEL: info
|
||||
TOKEN_TTL: 99d
|
||||
|
||||
persistence:
|
||||
mongo:
|
||||
size: 5Gi
|
||||
storageClass: ""
|
||||
redis:
|
||||
size: 1Gi
|
||||
storageClass: ""
|
||||
@@ -1,3 +1,104 @@
|
||||
// MUI sx keys that have a native (system) prop equivalent.
|
||||
// Flagged when found inside an sx={{ ... }} object literal — see frontend-conventions.md rule #1.
|
||||
const SX_NATIVE_EQUIVALENT_KEYS = [
|
||||
"color",
|
||||
"bgcolor",
|
||||
"backgroundColor",
|
||||
"borderRadius",
|
||||
"border",
|
||||
"borderTop",
|
||||
"borderBottom",
|
||||
"borderLeft",
|
||||
"borderRight",
|
||||
"width",
|
||||
"height",
|
||||
"minHeight",
|
||||
"maxWidth",
|
||||
"minWidth",
|
||||
"maxHeight",
|
||||
"p",
|
||||
"pt",
|
||||
"pb",
|
||||
"px",
|
||||
"py",
|
||||
"pl",
|
||||
"pr",
|
||||
"m",
|
||||
"mt",
|
||||
"mb",
|
||||
"mx",
|
||||
"my",
|
||||
"ml",
|
||||
"mr",
|
||||
"display",
|
||||
"textAlign",
|
||||
"fontWeight",
|
||||
"fontSize",
|
||||
"lineHeight",
|
||||
"gap",
|
||||
];
|
||||
|
||||
// MUI components that accept system props as top-level attributes.
|
||||
// sx={{ color, p, mt, ... }} on these can be hoisted into native props.
|
||||
// Dialog*, Drawer*, Menu*, Tab*, Toolbar etc. only accept sx — don't flag those.
|
||||
const SX_HOISTABLE_COMPONENTS = [
|
||||
"Box",
|
||||
"Stack",
|
||||
"Typography",
|
||||
"Grid",
|
||||
"Container",
|
||||
"Paper",
|
||||
"Card",
|
||||
"CardContent",
|
||||
"CardActions",
|
||||
"CardHeader",
|
||||
"Divider",
|
||||
"Link",
|
||||
"Chip",
|
||||
];
|
||||
|
||||
const sxKeySelector = SX_HOISTABLE_COMPONENTS.flatMap((component) =>
|
||||
SX_NATIVE_EQUIVALENT_KEYS.map(
|
||||
(k) =>
|
||||
`JSXOpeningElement[name.name='${component}'] > JSXAttribute[name.name='sx'] > JSXExpressionContainer > ObjectExpression > Property[key.name='${k}']`
|
||||
)
|
||||
).join(", ");
|
||||
|
||||
const FRONTEND_CONVENTION_RULES = {
|
||||
// Rule #2 — color/bgcolor/borderColor as a string shorthand like "text.secondary".
|
||||
// Catches: <Typography color="text.secondary" /> — should be color={theme.palette.text.secondary}.
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
selector:
|
||||
"JSXAttribute[name.name=/^(color|bgcolor|borderColor)$/] > Literal[value=/\\./]",
|
||||
message:
|
||||
'Use the full theme path instead of a string shorthand. e.g. color={theme.palette.text.secondary} not color="text.secondary". See docs/frontend-conventions.md rule #2.',
|
||||
},
|
||||
{
|
||||
// Rule #1 — sx={{ key: ... }} where key has a native prop equivalent.
|
||||
selector: sxKeySelector,
|
||||
message:
|
||||
"Hoist this key out of `sx` onto the component as a native MUI prop (e.g. color={...}, mt={...}, width={...}). See docs/frontend-conventions.md rule #1.",
|
||||
},
|
||||
{
|
||||
// Rule #4 — importing the singleton theme from the Theme module.
|
||||
// Catches: import { theme } from "@/Utils/Theme/Theme" — should be useTheme() inside the component.
|
||||
selector:
|
||||
"ImportDeclaration[source.value='@/Utils/Theme/Theme'] > ImportSpecifier[imported.name='theme']",
|
||||
message:
|
||||
"Don't import { theme } from @/Utils/Theme/Theme — use the useTheme() hook inside the component. See docs/frontend-conventions.md rule #4.",
|
||||
},
|
||||
{
|
||||
// Rule #3 (subset) — Typography fontSize={13} is the body default; drop it.
|
||||
selector:
|
||||
"JSXOpeningElement[name.name='Typography'] JSXAttribute[name.name='fontSize'] > JSXExpressionContainer > Literal[value=13]",
|
||||
message:
|
||||
"fontSize={13} is the Typography body default — drop the prop. For other sizes use typographyLevels tokens. See docs/frontend-conventions.md rule #3.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
@@ -15,9 +116,43 @@ module.exports = {
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"react/no-unescaped-entities": "off",
|
||||
...FRONTEND_CONVENTION_RULES,
|
||||
},
|
||||
globals: {
|
||||
__APP_VERSION__: "readonly",
|
||||
process: "readonly",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
plugins: ["@typescript-eslint", "react-refresh"],
|
||||
rules: {
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
],
|
||||
// TS handles these; eslint duplicates create noise on existing files.
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
...FRONTEND_CONVENTION_RULES,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
Checkmate
|
||||
Copyright (c) BlueWave Labs
|
||||
|
||||
This product includes assets developed by third parties.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
King chess-piece silhouette used in the Checkmate logo
|
||||
(client/src/assets/icons/checkmate-icon.svg and
|
||||
client/public/checkmate_favicon.svg)
|
||||
is adapted from "Chess klt45.svg" by Cburnett, available at:
|
||||
https://commons.wikimedia.org/wiki/File:Chess_klt45.svg
|
||||
Licensed under Creative Commons Attribution-ShareAlike 3.0 Unported
|
||||
(CC BY-SA 3.0): https://creativecommons.org/licenses/by-sa/3.0/
|
||||
|
||||
The original artwork has been recolored from the white-with-black-stroke
|
||||
chess piece to the Checkmate brand green (#13715B) for use as a logo.
|
||||
Modifications: stroke color change only; geometry unchanged.
|
||||
Generated
+931
-218
File diff suppressed because it is too large.
Load diff
+11
-7
@@ -1,21 +1,23 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "3.9.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build-dev": "tsc -b && vite build --mode development",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint": "eslint . --ext js,jsx,ts,tsx --report-unused-disable-directives",
|
||||
"lint-strict": "eslint . --ext js,jsx,ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check ."
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*": "prettier --write"
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"dependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
@@ -26,13 +28,12 @@
|
||||
"@mui/x-date-pickers": "7.29.4",
|
||||
"@reduxjs/toolkit": "2.7.0",
|
||||
"@types/maplibre-gl": "^1.13.2",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.15.2",
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"human-interval": "2.0.1",
|
||||
"i18next": "25.4.2",
|
||||
"joi": "17.13.3",
|
||||
"joi": "17.13.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "^5.19.0",
|
||||
"mui-color-input": "^7.0.0",
|
||||
@@ -58,14 +59,17 @@
|
||||
"@types/node": "24.5.2",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"lint-staged": "^16.2.7",
|
||||
"mongoose": "9.6.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "6.4.1"
|
||||
"vite": "^6.4.2"
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,11 @@
|
||||
<svg width="32" height="32" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M218.252 79.2667L218.045 78.4143C218.712 77.617 219.642 77.5282 220.615 77.4361C224.829 77.0374 243.307 76.7077 246.676 77.418C248.299 77.76 249.661 78.5248 250.932 79.5734C254.845 82.8003 259.018 88.0302 262.403 91.9284C263.57 93.2734 264.805 95.1623 266.183 96.2584C268.203 99.7185 274.532 101.602 278.167 102.623C278.397 102.688 278.631 102.741 278.863 102.797C274.983 106.185 272.016 110.321 268.272 113.745C272.574 124.188 286.689 184.953 289.397 187.986C290.165 188.212 290.291 188.048 291.045 187.739C290.627 188.446 290.337 189.3 289.588 189.656L288.963 188.533C286.703 188.347 273.044 195.497 269.836 196.808C267.39 197.808 264.851 198.458 262.414 199.405C261.197 196.797 260.413 193.923 259.329 191.244C254.43 179.142 250.091 166.843 245.71 154.552C240.241 139.206 234.226 124.026 228.882 108.642C226.389 101.466 223.549 94.5009 220.953 87.3749C220.026 84.8301 219.72 81.4795 218.252 79.2667Z" fill="#3158B7"/>
|
||||
<path d="M218.252 79.2667L218.045 78.4143C218.712 77.617 219.642 77.5282 220.615 77.4361C224.829 77.0374 243.307 76.7077 246.676 77.418C248.299 77.76 249.661 78.5248 250.932 79.5734C254.845 82.8003 259.018 88.0302 262.403 91.9284C263.57 93.2734 264.805 95.1623 266.183 96.2584C268.203 99.7185 274.532 101.602 278.167 102.623C278.397 102.688 278.631 102.741 278.863 102.797C274.983 106.185 272.016 110.321 268.272 113.745C266.08 111.774 263.588 110.159 261.208 108.427C256.477 104.982 251.692 101.649 246.906 98.282L233.54 88.9767C228.729 85.5908 223.823 81.3267 218.252 79.2667Z" fill="#73E4FA"/>
|
||||
<path d="M299.595 19.4005C302.177 17.8183 305.226 16.8946 307.955 15.5556C310.387 14.3626 312.673 12.8604 315.15 11.7619C315.624 11.5519 315.886 11.4697 316.398 11.4844C315.732 13.7848 316.431 26.6622 316.441 30.2652C316.474 42.2192 316.084 54.2432 316.385 66.1867C316.416 67.4343 316.377 68.6301 316.847 69.8026C316.457 79.0631 316.718 88.4086 316.651 97.6808L316.423 172.062C316.376 177.75 316.385 183.438 316.448 189.125C316.469 192.585 316.902 196.888 316.126 200.243C316.055 199.033 315.728 197.334 316.025 196.183C315.86 197.435 315.463 198.762 315.391 200.002C315.283 201.847 315.798 203.719 315.391 205.559C314.431 204.554 313.43 203.906 312.592 202.729C308.228 196.602 310.556 194.07 301.963 190.683C299.704 189.793 293.054 186.88 291.045 187.739C290.291 188.048 290.165 188.212 289.397 187.986C286.689 184.953 272.574 124.188 268.272 113.745C272.016 110.321 274.983 106.185 278.863 102.797C278.631 102.741 278.397 102.688 278.167 102.623C274.532 101.602 268.203 99.7184 266.183 96.2583C269.435 97.3504 272.901 97.5882 276.304 97.6583C280.306 97.7408 283.403 97.3026 286.383 94.3544C288.086 92.669 289.516 90.6569 291.136 88.8839C293.303 86.512 296.041 84.1801 297.633 81.3777C300.012 77.1924 299.115 62.4967 299.219 57.0841C296.538 56.7086 293.912 56.806 291.218 56.6995C288.184 56.5796 285.396 56.0507 283.294 53.6585C280.886 50.9188 280.696 47.3466 281.035 43.8756C282.423 29.6657 295.122 37.239 298.804 33.8184C299.712 32.9739 299.469 21.618 299.595 19.4005Z" fill="#3D7ECD"/>
|
||||
<path d="M362.288 269.421L362.773 270.062C364.332 274.37 365.193 279.073 366.359 283.516C368.419 291.371 370.61 299.194 372.793 307.015L395.769 388.039L411.304 442.863L415.766 458.213C416.528 460.836 417.569 463.467 417.96 466.175C417.89 466.842 418.03 466.869 417.485 467.324C413.349 466.406 409.11 465.92 404.942 465.147C396.892 463.653 388.881 461.984 380.817 460.552C380.516 459.221 380.489 457.712 380.305 456.347C379.896 453.327 379.359 450.33 378.694 447.356C376.712 438.738 374.16 430.227 372.075 421.626L351.327 334.35C349.071 324.725 347.019 314.981 344.607 305.405C342.561 297.284 340.078 289.201 338.622 280.946C339.912 280.649 340.991 280.35 342.142 279.684C345.072 278.136 348.264 277.064 351.221 275.536C354.9 273.636 358.345 270.649 362.288 269.421Z" fill="#73E4FA"/>
|
||||
<path d="M316.398 11.4845C316.949 11.6168 317.496 11.7682 318.021 11.982C320.527 13.0022 322.595 14.8975 325.025 16.0401C326.641 16.7993 328.506 17.0278 330.066 17.9189C330.615 18.2326 331.175 18.6401 331.516 19.1833C333.638 22.5617 329.941 33.143 334.144 34.5595C335.071 34.872 336.309 34.7269 337.278 34.6636C342.517 34.3216 346.04 34.3235 349.065 39.1827C350.891 42.117 351.841 45.3207 350.982 48.7564C350.164 52.0314 347.831 55.4635 344.902 57.2399C344.309 57.5993 343.657 57.8308 343.003 58.0488C342.382 58.2541 341.729 58.4831 341.176 58.8402C339.34 60.0257 337.705 61.6472 335.85 62.8819C335.083 63.3916 333.536 63.9343 333.114 64.7163C333.015 64.8989 332.938 65.0921 332.849 65.2797C331.994 71.778 330.942 74.7954 335.242 80.4525C338.105 84.2196 345.676 93.407 349.248 96.0711C350.329 96.8768 351.548 97.3899 352.858 97.674C354.638 97.679 356.567 97.8353 358.319 97.4932C366.986 95.8002 372.945 85.6789 379.155 80.1412C380.557 78.8916 382.117 77.8947 383.974 77.4926C387.385 76.754 391.296 77.2429 394.773 77.2627C400.744 77.2966 406.631 77.1509 412.603 77.4939C413.066 77.8038 413.08 77.6663 413.16 78.1487C413.385 79.5036 412.832 81.0938 412.41 82.377C409.805 90.2864 406.49 98.1119 403.607 105.936L387.525 150.29C383.617 161.269 379.569 172.195 375.381 183.07C373.017 189.241 370.826 195.663 367.756 201.518C366.906 200.846 366.105 200.384 365.115 199.946C357.118 196.547 349.468 191.947 341.757 187.926C341.211 187.423 341.102 187.207 340.301 187.318C338.422 187.579 336.346 188.419 334.502 188.929C330.434 190.055 321.796 191.764 319.469 195.652C318.572 197.151 317.692 200.22 316.337 201.245C316.225 200.886 316.145 200.621 316.126 200.243C316.902 196.888 316.469 192.585 316.448 189.125C316.385 183.438 316.377 177.75 316.423 172.062L316.651 97.6809C316.718 88.4087 316.457 79.0632 316.847 69.8027C316.377 68.6302 316.416 67.4344 316.385 66.1868C316.084 54.2433 316.474 42.2193 316.441 30.2653C316.431 26.6623 315.732 13.7849 316.398 11.4845Z" fill="#73E4FA"/>
|
||||
<path d="M332.127 57.4141C335.751 57.5106 339.378 57.8905 343.003 58.0488C342.382 58.2541 341.729 58.4831 341.176 58.8402C339.34 60.0257 337.705 61.6472 335.85 62.8819C335.083 63.3916 333.536 63.9343 333.114 64.7163C333.015 64.8989 332.938 65.0921 332.849 65.2797C332.684 62.6459 332.422 60.0356 332.127 57.4141Z" fill="black"/>
|
||||
<path d="M352.858 97.674C354.638 97.679 356.567 97.8353 358.319 97.4932C366.986 95.8002 372.945 85.6789 379.155 80.1412C380.557 78.8916 382.117 77.8947 383.974 77.4926C387.385 76.754 391.296 77.2429 394.773 77.2627C400.744 77.2966 406.631 77.1509 412.603 77.4939C412.123 77.5931 411.776 77.6589 411.35 77.9133C409.284 79.1482 407.188 81.3404 405.081 82.7802C394.559 89.9716 384.283 97.4873 373.787 104.715C371.042 106.605 368.398 108.696 365.587 110.481C363.016 106.042 360.879 102.667 356.684 99.576C355.908 99.004 354.963 98.1848 354.042 97.8844C353.777 97.7979 353.467 97.7915 353.199 97.7482C353.084 97.7297 352.972 97.6988 352.858 97.674Z" fill="#4F91E0"/>
|
||||
<path d="M316.847 69.8027C321.242 72.4105 326.84 79.7356 330.518 83.5726C335.063 88.3145 340.305 92.4625 345.078 96.9857C348.754 100.47 359.102 112.197 362.531 113.979C362.884 116.416 347.596 173.131 345.411 179.979C344.657 182.345 343.764 185.35 342.254 187.338C342.099 187.543 341.927 187.734 341.757 187.926C341.211 187.423 341.102 187.207 340.301 187.318C338.422 187.579 336.346 188.419 334.502 188.929C330.434 190.055 321.796 191.764 319.469 195.652C318.572 197.151 317.692 200.22 316.337 201.245C316.225 200.886 316.145 200.621 316.126 200.243C316.902 196.888 316.469 192.585 316.448 189.125C316.385 183.438 316.376 177.75 316.423 172.062L316.651 97.6809C316.718 88.4087 316.457 79.0632 316.847 69.8027Z" fill="#5BBFFC"/>
|
||||
<path d="M316.126 200.243C316.145 200.621 316.225 200.886 316.337 201.245C317.692 200.22 318.572 197.151 319.469 195.652C321.796 191.764 330.434 190.055 334.502 188.929C336.346 188.419 338.422 187.579 340.301 187.318C341.102 187.207 341.211 187.423 341.757 187.926C349.468 191.947 357.118 196.547 365.115 199.946C366.105 200.384 366.906 200.846 367.756 201.518C367.27 202.637 366.763 203.747 366.234 204.848C365.063 207.595 364.344 210.137 363.459 212.952C367.613 213.364 371.797 213.297 375.966 213.194C386.171 213.341 399.134 211.56 407.006 219.438C408.262 220.694 409.583 222.077 410.315 223.707C411.447 228.7 410.939 234.09 410.692 239.173C410.661 239.285 410.628 239.397 410.599 239.511C410.317 240.612 410.264 241.816 409.76 242.846C408.269 245.89 403.918 248.67 401.099 250.509L397.101 250.535C392.229 251.156 386.936 250.764 382.02 250.742C380.258 252.242 379.572 253.4 379.015 255.621C375.623 257.067 372.349 258.186 368.836 259.31C366.357 260.996 362.191 264.345 361.869 267.495C361.803 268.145 361.893 268.578 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965L337.631 281.175C336.996 280.978 336.122 280.707 335.64 280.22C335.013 279.586 332.112 273.818 330.826 272.131C327.237 267.42 322.458 264.909 316.638 264.129L316.192 264.037C314.993 265.535 316.083 270.149 315.579 272.213C315.076 269.916 316.559 267.352 315.272 265.334C314.562 264.951 314 264.949 313.234 265.15C310.643 265.828 308.244 267.144 305.601 267.663L305.359 267.78C304.526 268.174 303.726 268.445 302.836 268.679L302.422 268.516C302.97 267.994 303.581 267.707 304.249 267.371C303.346 267.089 302.592 267.405 301.795 267.828C300.798 268.358 299.724 269.109 298.638 269.418C296.45 270.04 293.748 268.782 292.771 271.723C291.539 275.429 292.836 276.598 289.342 279.245L289.264 279.106L289.746 279.665C289.922 279.642 290.098 279.617 290.274 279.596C290.788 279.534 291.072 279.562 291.552 279.784C283.761 280.886 276.08 271.943 269.481 268.727C269.532 267.685 269.414 267.036 268.848 266.155C266.793 262.951 261.551 259.21 258.011 257.869C256.773 257.4 255.382 257.17 254.087 256.899C250.725 256.497 247.482 255.358 244.173 254.665C241.651 254.137 239.029 253.962 236.466 253.728C236.969 252.907 236.631 251.749 236.978 250.742C234.444 250.731 231.884 250.523 229.351 250.403C226.903 249.086 224.175 246.485 222.644 244.15C220.166 240.368 220.112 229.455 221.195 225.05C221.962 221.93 223.946 219.508 226.648 217.845C227.525 217.021 228.391 216.267 229.547 215.874C232.606 214.832 236.233 214.827 239.433 214.547C243.327 214.206 247.238 213.733 251.141 213.538C256.502 213.269 261.883 213.36 267.25 213.257C267.293 213.167 267.345 213.08 267.378 212.986C268.103 210.948 263.421 201.839 262.414 199.405C264.851 198.458 267.39 197.808 269.836 196.808C273.044 195.497 286.703 188.347 288.963 188.533L289.588 189.656C290.337 189.3 290.627 188.446 291.045 187.739C293.054 186.88 299.704 189.793 301.963 190.683C310.556 194.07 308.228 196.602 312.592 202.729C313.43 203.906 314.431 204.554 315.391 205.559C315.798 203.719 315.283 201.847 315.391 200.002C315.463 198.762 315.86 197.435 316.025 196.183C315.728 197.334 316.055 199.033 316.126 200.243Z" fill="#73E4FA"/>
|
||||
<path d="M226.648 217.845C227.525 217.021 228.391 216.267 229.547 215.874C232.606 214.832 236.233 214.827 239.433 214.547C243.327 214.206 247.238 213.733 251.141 213.538C256.502 213.269 261.883 213.36 267.25 213.257C273.5 213.971 279.834 213.445 286.108 213.605C286.028 213.626 285.948 213.648 285.867 213.666C283.762 214.153 281.367 213.997 279.208 214.037C272.251 214.165 265.326 213.83 258.375 213.88C253.031 213.919 247.886 214.67 242.578 215.017C240.895 215.127 236.024 214.805 235.01 216.105C234.814 216.356 234.778 216.499 234.672 216.773C235.26 217.358 235.736 217.319 236.534 217.341C240.179 217.439 243.921 217.212 247.585 217.238C254.057 217.283 260.516 217.54 266.99 217.551C270.978 217.557 278.548 217.235 282.283 217.919C273.611 218.558 264.721 217.948 256.023 218.03C249.71 217.873 243.396 217.847 237.081 217.954C233.732 217.991 229.946 218.398 226.648 217.845Z" fill="#4F91E0"/>
|
||||
<path d="M321 196.5C321.019 196.878 320.86 194.141 320.972 194.5C322.327 193.475 318.572 197.151 319.469 195.652C321.796 191.764 330.434 190.055 334.502 188.929C336.346 188.419 338.422 187.579 340.301 187.318C341.102 187.207 341.211 187.423 341.757 187.926C349.468 191.947 357.118 196.547 365.115 199.946C366.105 200.384 366.906 200.846 367.756 201.518C367.27 202.637 366.763 203.747 366.234 204.848C365.063 207.595 364.344 210.137 363.459 212.952L361.76 212.951C346.809 213.807 331.759 213.014 316.787 213.074L315.391 205.559C315.798 203.719 315.283 201.847 315.391 200.002C315.463 198.762 315.835 196.752 316 195.5C315.5 195 323.374 191.757 321 196.5Z" fill="#3158B7"/>
|
||||
<path d="M291.045 187.739C293.054 186.88 299.704 189.793 301.963 190.683C310.556 194.07 305.5 191.5 312.5 194.5C314 195 315.54 193.995 316.5 195L316.787 213.074C306.586 212.996 296.304 213.296 286.108 213.605C279.834 213.445 273.5 213.971 267.25 213.257C267.293 213.167 267.345 213.08 267.378 212.986C268.103 210.948 263.421 201.839 262.414 199.405C264.851 198.458 267.39 197.808 269.836 196.808C273.044 195.497 286.703 188.347 288.963 188.533L289.5 188.5C290.249 188.144 290.627 188.446 291.045 187.739Z" fill="#15337C"/>
|
||||
<path d="M316.158 217.961C333.626 218.066 351.372 216.648 368.745 218.663C369.093 229.235 368.662 239.88 368.76 250.467L387.46 250.44C390.609 250.437 393.982 250.193 397.101 250.535C392.229 251.156 386.936 250.764 382.02 250.742L316.217 250.777C316.034 247.101 316.2 243.334 316.205 239.649C316.23 232.42 316.215 225.19 316.158 217.961Z" fill="#5BBFFC"/>
|
||||
<path d="M229.351 250.403C226.903 249.086 224.175 246.485 222.644 244.15C220.166 240.368 220.112 229.455 221.195 225.05C221.962 221.93 223.946 219.508 226.648 217.845C229.946 218.398 233.732 217.991 237.081 217.954C243.396 217.847 249.71 217.873 256.023 218.03C264.721 217.948 273.611 218.558 282.283 217.919L316.158 217.961C316.215 225.19 316.23 232.42 316.205 239.649C316.2 243.334 316.034 247.101 316.217 250.777L382.02 250.742C380.258 252.242 379.572 253.4 379.015 255.621C375.623 257.067 372.349 258.186 368.836 259.31C366.357 260.996 362.191 264.345 361.869 267.495C361.803 268.145 361.893 268.578 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965L337.631 281.175C336.996 280.978 336.122 280.707 335.64 280.22C335.013 279.586 332.112 273.818 330.826 272.131C327.237 267.42 322.458 264.909 316.638 264.129L316.192 264.037C314.993 265.535 316.083 270.149 315.579 272.213C315.076 269.916 316.559 267.352 315.272 265.334C314.562 264.951 314 264.949 313.234 265.15C310.643 265.828 308.244 267.144 305.601 267.663L305.359 267.78C304.526 268.174 303.726 268.445 302.836 268.679L302.422 268.516C302.97 267.994 303.581 267.707 304.249 267.371C303.346 267.089 302.592 267.405 301.795 267.828C300.798 268.358 299.724 269.109 298.638 269.418C296.45 270.04 293.748 268.782 292.771 271.723C291.539 275.429 292.836 276.598 289.342 279.245L289.264 279.106L289.746 279.665C289.922 279.642 290.098 279.617 290.274 279.596C290.788 279.534 291.072 279.562 291.552 279.784C283.761 280.886 276.08 271.943 269.481 268.727C269.532 267.685 269.414 267.036 268.848 266.155C266.793 262.951 261.551 259.21 258.011 257.869C256.773 257.4 255.382 257.17 254.087 256.899C250.725 256.497 247.482 255.358 244.173 254.665C241.651 254.137 239.029 253.962 236.466 253.728C236.969 252.907 236.631 251.749 236.978 250.742C234.444 250.731 231.884 250.523 229.351 250.403Z" fill="#3D7ECD"/>
|
||||
<path d="M229.5 250.5C227.052 249.183 224.175 246.485 222.644 244.15C220.166 240.368 220.112 229.455 221.195 225.05C221.962 221.93 223.946 219.508 226.648 217.845C229.946 218.398 233.732 217.991 237.081 217.954C243.396 217.847 249.71 217.873 256.023 218.03C258.057 218.243 260.081 218.239 262.124 218.245C262.036 228.982 261.779 239.713 261.935 250.452C267.117 250.445 282.096 249.873 286.095 250.724C281.121 250.989 276.017 250.759 271.029 250.75C261.584 250.723 252.14 250.748 242.696 250.824C240.816 250.676 238.867 250.759 236.978 250.742C234.444 250.731 232.033 250.62 229.5 250.5Z" fill="#3158B7"/>
|
||||
<path d="M242.696 250.824C252.14 250.748 261.584 250.723 271.029 250.75C276.017 250.759 281.121 250.989 286.095 250.724C296.126 250.796 306.188 250.976 316.217 250.777L382.02 250.742C380.258 252.242 379.572 253.4 379.015 255.621C375.623 257.067 372.349 258.186 368.836 259.31C366.357 260.996 362.191 264.345 361.869 267.495C361.803 268.145 361.893 268.578 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965L337.631 281.175C336.996 280.978 336.122 280.707 335.64 280.22C335.013 279.586 332.112 273.818 330.826 272.131C327.237 267.42 322.458 264.909 316.638 264.129L316.192 264.037C314.993 265.535 316.083 270.149 315.579 272.213C315.076 269.916 316.559 267.352 315.272 265.334C314.562 264.951 314 264.949 313.234 265.15C310.643 265.828 308.244 267.144 305.601 267.663L305.359 267.78C304.526 268.174 303.726 268.445 302.836 268.679L302.422 268.516C302.97 267.994 303.581 267.707 304.249 267.371C303.346 267.089 302.592 267.405 301.795 267.828C300.798 268.358 299.724 269.109 298.638 269.418C296.45 270.04 293.748 268.782 292.771 271.723C291.539 275.429 292.836 276.598 289.342 279.245L289.264 279.106L289.746 279.665C289.922 279.642 290.098 279.617 290.274 279.596C290.788 279.534 291.072 279.562 291.552 279.784C283.761 280.886 276.08 271.943 269.481 268.727C269.532 267.685 269.414 267.036 268.848 266.155C266.793 262.951 261.551 259.21 258.011 257.869C256.773 257.4 255.382 257.17 254.087 256.899C250.725 256.497 247.482 255.358 244.173 254.665C241.651 254.137 239.029 253.962 236.466 253.728C236.969 252.907 236.631 251.749 236.978 250.742C238.867 250.759 240.816 250.676 242.696 250.824Z" fill="url(#paint0_linear_4222_6997)"/>
|
||||
<path d="M236.978 250.742C238.867 250.759 240.816 250.676 242.696 250.824C242.881 250.874 243.066 250.928 243.252 250.974C245.298 251.479 247.92 251.212 250.051 251.224C248.443 251.53 247.073 251.291 245.943 252.702C245.85 253.55 245.913 253.633 246.31 254.399C248.336 255.702 252.778 255.161 254.087 256.899C250.725 256.497 247.482 255.358 244.173 254.665C241.651 254.137 239.029 253.962 236.466 253.728C236.969 252.907 236.631 251.749 236.978 250.742Z" fill="#15337C"/>
|
||||
<path d="M315.839 263.948C315.537 262.501 315.708 261.013 315.229 259.629C314.46 258.966 313.976 259.049 312.984 258.933C313.102 258.884 313.213 258.814 313.338 258.787C315.769 258.246 325.519 258.983 328.801 258.983L357.499 259.017C359.803 259.006 366.156 258.505 368.097 258.96C368.485 259.051 368.546 259.119 368.836 259.31C366.357 260.996 362.191 264.345 361.869 267.495C361.803 268.145 361.893 268.578 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965L337.631 281.175C336.996 280.978 336.122 280.707 335.64 280.22C335.013 279.586 332.112 273.818 330.826 272.131C327.237 267.42 322.458 264.909 316.638 264.129L316.192 264.037C314.993 265.535 316.083 270.149 315.579 272.213C315.076 269.916 316.559 267.352 315.272 265.334C314.562 264.951 314 264.949 313.234 265.15C310.643 265.828 308.244 267.144 305.601 267.663C307.941 265.704 312.848 264.583 315.839 263.948Z" fill="url(#paint1_linear_4222_6997)"/>
|
||||
<path d="M334.406 268.36C340.718 267.927 347.148 267.514 353.456 268.191C355.699 268.431 360.368 269.793 362.175 269.155L362.288 269.421C358.345 270.649 354.9 273.636 351.221 275.536C348.264 277.064 345.072 278.136 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965C338.013 279.917 337.843 278.75 337.605 277.665C336.155 274.684 336.233 271.392 334.406 268.36Z" fill="#4F91E0"/>
|
||||
<path d="M337.605 277.665C338.101 277.244 338.105 277.263 338.759 277.253C340.026 277.925 341.077 278.732 342.142 279.684C340.991 280.35 339.912 280.649 338.622 280.946L338.374 280.965C338.013 279.917 337.843 278.75 337.605 277.665Z" fill="#5BBFFC"/>
|
||||
<path d="M162.994 506.672C163.51 506.136 164.063 505.686 164.643 505.225C167.325 506.032 178.659 505.485 182.142 505.49L221.176 505.555C226.755 505.582 232.376 505.443 237.949 505.628C269.102 506.183 300.329 505.731 331.491 505.742L372.011 505.764C380.199 505.77 388.457 505.962 396.635 505.618C396.442 517.711 396.022 529.884 396.433 541.975C409.395 542.082 422.357 542.091 435.32 542.002C441.062 542.005 447.41 541.469 453.076 542.23C457.801 542.254 462.724 542.135 467.419 542.679C465.179 543.78 463.234 548.572 462.043 550.796V551.298L462.794 551.648C454.827 551.784 446.872 552.172 438.929 552.813C436.511 552.995 434.06 552.553 431.644 552.413C428.22 552.214 424.779 552.237 421.352 552.25C402.933 552.322 384.531 552.526 366.11 552.346L227.563 552.274C215.642 552.277 203.672 552.344 191.757 552.13C182.178 551.958 172.491 551.389 162.923 551.866C164.053 549.046 163.345 544.905 162.658 542.024C162.018 536.512 162.277 530.737 162.288 525.186C162.299 519.081 161.931 512.699 162.994 506.672Z" fill="#15337C"/>
|
||||
<path d="M162.994 506.672C163.51 506.136 164.063 505.686 164.643 505.225C167.325 506.032 178.659 505.485 182.142 505.49L221.176 505.555C226.755 505.582 232.376 505.443 237.949 505.628C237.694 517.722 237.219 529.932 237.571 542.021L396.433 541.975C409.395 542.082 422.357 542.091 435.32 542.002C441.062 542.005 447.41 541.469 453.076 542.23C445.432 542.447 437.752 542.287 430.104 542.288L384.066 542.238L236.646 542.27L181.968 542.237C175.536 542.231 169.083 541.914 162.658 542.024C162.018 536.512 162.277 530.737 162.288 525.186C162.299 519.081 161.931 512.699 162.994 506.672Z" fill="#3158B7"/>
|
||||
<path d="M237.949 505.628C269.102 506.183 300.329 505.731 331.491 505.742L372.011 505.764C380.199 505.77 388.457 505.962 396.635 505.618C396.442 517.711 396.022 529.884 396.433 541.975L237.571 542.021C237.219 529.932 237.694 517.722 237.949 505.628Z" fill="#3D7ECD"/>
|
||||
<path d="M316.173 450.214L316.356 450.171C327.599 451.012 338.68 453.515 349.747 455.526C360.085 457.318 370.442 458.993 380.817 460.552C388.881 461.984 396.892 463.653 404.942 465.147C409.11 465.92 413.349 466.406 417.485 467.324C418.03 466.869 417.89 466.842 417.96 466.175C418.868 471.139 419.01 474.417 418.882 479.505C424.212 480.991 428.904 484.71 433.752 487.295L467.07 504.914C467.988 505.502 468.749 505.944 468.955 507.111C469.514 510.282 469.469 539.486 468.739 541.824C468.292 542.267 468.021 542.487 467.419 542.679C462.724 542.135 457.801 542.254 453.076 542.23C447.41 541.469 441.062 542.005 435.32 542.002C422.357 542.091 409.395 542.082 396.433 541.975C396.022 529.884 396.442 517.711 396.635 505.618C388.457 505.962 380.199 505.77 372.011 505.764L331.491 505.742C300.329 505.731 269.102 506.183 237.949 505.628C232.376 505.443 226.755 505.582 221.176 505.555L182.142 505.49C178.659 505.485 167.325 506.032 164.643 505.225C166.684 503.613 169.31 502.543 171.632 501.378C176.276 499.048 181.007 496.831 185.604 494.419C192.121 490.999 198.566 487.372 204.999 483.795C207.005 482.679 208.919 481.328 210.961 480.297C211.664 479.943 212.437 479.729 213.192 479.519C213.447 477.939 213.28 476.121 213.292 474.511C213.314 471.755 213.495 469.182 213.961 466.462C214.147 466.81 214.01 466.597 214.452 467.044C227.245 465.759 239.827 462.451 252.483 460.143C260.461 458.819 268.511 457.946 276.475 456.519L300.283 452.407C305.401 451.522 310.98 450.069 316.173 450.214Z" fill="#73E4FA"/>
|
||||
<path d="M316.173 450.214L316.356 450.171C327.599 451.012 338.68 453.515 349.747 455.526C360.085 457.318 370.442 458.993 380.817 460.552C388.881 461.984 396.892 463.653 404.942 465.147C409.11 465.92 413.349 466.406 417.485 467.324C418.03 466.869 417.89 466.842 417.96 466.175C418.868 471.139 419.01 474.417 418.882 479.505C407.542 478.716 395.89 479.365 384.514 479.436C370.443 479.525 356.374 479.202 342.302 479.225L326.141 479.166C322.94 479.164 319.619 479.396 316.441 479.069C316.286 469.43 316.022 459.862 316.173 450.214Z" fill="#3D7ECD"/>
|
||||
<path d="M467.07 504.914C467.988 505.502 468.749 505.944 468.955 507.111C469.514 510.282 469.469 539.486 468.739 541.824C468.292 542.267 468.021 542.487 467.419 542.679C462.724 542.135 457.801 542.254 453.076 542.23C447.41 541.469 441.062 542.005 435.32 542.002C422.357 542.091 409.395 542.082 396.433 541.975C396.022 529.884 396.442 517.711 396.635 505.618C399.774 505.933 403.063 505.785 406.221 505.786L422.299 505.784C435.31 505.785 448.476 506.228 461.464 505.68C463.339 505.6 465.314 505.634 467.07 504.914Z" fill="#5BBFFC"/>
|
||||
<path d="M252.483 460.143C260.461 458.819 268.511 457.946 276.475 456.519L300.283 452.407C305.401 451.522 310.98 450.069 316.173 450.214C316.022 459.862 316.286 469.43 316.441 479.069C313.41 479.108 310.372 479.425 307.371 479.847C292.468 483.6 277.556 488.899 263.025 493.906C257.708 495.738 251.924 497.128 246.801 499.408C243.673 500.8 240.545 502.657 238.239 505.216L351.838 505.253L382.181 505.431C386.77 505.429 392.191 504.724 396.635 505.618C388.457 505.962 380.199 505.77 372.011 505.764L331.491 505.742C300.329 505.731 269.102 506.183 237.949 505.628C232.376 505.443 226.755 505.582 221.176 505.555L182.142 505.49C178.659 505.485 167.325 506.032 164.643 505.225C166.684 503.613 169.31 502.543 171.632 501.378C176.276 499.048 181.007 496.831 185.604 494.419C192.121 490.999 198.566 487.372 204.999 483.795C207.005 482.679 208.919 481.328 210.961 480.297C211.664 479.943 212.437 479.729 213.192 479.519C213.447 477.939 213.28 476.121 213.292 474.511C213.314 471.755 213.495 469.182 213.961 466.462C214.147 466.81 214.01 466.597 214.452 467.044C227.245 465.759 239.827 462.451 252.483 460.143Z" fill="url(#paint2_linear_4222_6997)"/>
|
||||
<path d="M252.483 460.143C260.461 458.819 268.511 457.946 276.475 456.519L300.283 452.407C305.401 451.522 310.98 450.069 316.173 450.214C316.022 459.862 316.286 469.43 316.441 479.069C313.41 479.108 310.372 479.425 307.371 479.847C280.73 478.905 253.933 479.206 227.271 479.298C222.578 479.314 217.879 479.538 213.192 479.519C213.447 477.939 213.28 476.121 213.292 474.511C213.314 471.755 213.495 469.182 213.961 466.462C214.147 466.81 214.01 466.597 214.452 467.044C227.245 465.759 239.827 462.451 252.483 460.143Z" fill="#15337C"/>
|
||||
<path d="M305.601 267.663C308.244 267.144 310.643 265.828 313.234 265.15C314 264.949 314.562 264.951 315.272 265.334C316.559 267.352 315.076 269.916 315.579 272.213C316.083 270.149 314.993 265.535 316.192 264.037L316.638 264.129C322.458 264.909 327.237 267.42 330.826 272.131C332.112 273.818 335.013 279.586 335.64 280.22C336.122 280.707 336.996 280.978 337.631 281.175L338.374 280.965L338.622 280.946C340.078 289.201 342.561 297.284 344.607 305.405C347.019 314.981 349.071 324.725 351.327 334.35L372.075 421.626C374.16 430.227 376.712 438.738 378.694 447.356C379.359 450.33 379.896 453.327 380.305 456.347C380.489 457.712 380.516 459.221 380.817 460.552C370.442 458.993 360.085 457.318 349.747 455.526C338.68 453.515 327.599 451.012 316.356 450.171L316.173 450.214C310.98 450.069 305.401 451.522 300.283 452.407L276.475 456.519C268.511 457.946 260.461 458.819 252.483 460.143C239.827 462.451 227.245 465.759 214.452 467.044C214.01 466.597 214.147 466.81 213.961 466.462C217.335 455.731 219.76 444.734 222.73 433.889L237.945 379.405L269.481 268.727C276.08 271.943 283.761 280.886 291.552 279.784C291.072 279.562 290.788 279.534 290.274 279.596C290.098 279.617 289.922 279.642 289.746 279.665L289.264 279.106L289.342 279.245C292.836 276.598 291.539 275.429 292.771 271.723C293.748 268.782 296.45 270.04 298.638 269.418C299.724 269.109 300.798 268.358 301.795 267.828C302.592 267.405 303.346 267.089 304.249 267.371C303.581 267.707 302.97 267.994 302.422 268.516L302.836 268.679C303.726 268.445 304.526 268.174 305.359 267.78L305.601 267.663Z" fill="#3D7ECD"/>
|
||||
<path d="M213.961 466.462C217.335 455.731 219.76 444.734 222.73 433.889L237.945 379.405L269.481 268.727C276.08 271.943 283.761 280.886 291.552 279.784C294.318 279.786 297.526 280.103 300.239 279.671C302.32 279.34 293.35 281.165 295.5 281C292.765 282.129 294.815 282.321 291.94 282.461C291.8 282.468 291.66 282.481 291.519 282.489C291.435 282.494 291.35 282.496 291.266 282.5C290.329 287.67 289.473 292.882 288.349 298.015L265.21 399.097L256.243 437.602C255.364 441.302 250.844 457.078 251.4 459.744C251.766 459.902 252.097 460.037 252.483 460.143C239.827 462.451 227.245 465.759 214.452 467.044C214.01 466.597 214.147 466.81 213.961 466.462Z" fill="#3158B7"/>
|
||||
<path d="M316.638 264.129C322.458 264.909 327.237 267.42 330.826 272.131C332.112 273.818 335.013 279.586 335.64 280.22C336.122 280.707 336.996 280.978 337.631 281.175L338.374 280.965L338.622 280.946C340.078 289.201 342.561 297.284 344.607 305.405C347.019 314.981 349.071 324.725 351.327 334.35L372.075 421.626C374.16 430.227 376.712 438.738 378.694 447.356C379.359 450.33 379.896 453.327 380.305 456.347C380.489 457.712 380.516 459.221 380.817 460.552C370.442 458.993 360.085 457.318 349.747 455.526C338.68 453.515 327.599 451.012 316.356 450.171L316.124 267.318C316.132 266.225 316.016 265.039 316.638 264.129Z" fill="#5BBFFC"/>
|
||||
<path d="M162.602 542.012C222.681 542.667 282.903 542.133 343 542.146L421.144 542.172C436.934 542.179 452.86 542.406 468.631 542C468.259 556.284 468.631 576.5 466.168 585H164.5C162.602 574 162.11 556.297 162.602 542.012Z" fill="#2E57AB"/>
|
||||
<path d="M263.193 261.724L235.532 253.517L232.5 252.5L229.5 250.5L402 250L377.5 261.724L364 275.5L339.134 284H289.345L269.731 271.69L263.193 261.724Z" fill="#2950B6"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4222_6997" x1="313.33" y1="251.065" x2="313.326" y2="279.202" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#122869"/>
|
||||
<stop offset="1" stop-color="#2950B6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4222_6997" x1="342.025" y1="256.698" x2="339.592" y2="280.614" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3052A7"/>
|
||||
<stop offset="1" stop-color="#2671CE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4222_6997" x1="233.914" y1="479.018" x2="235.081" y2="504.885" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#486DC9"/>
|
||||
<stop offset="1" stop-color="#599BEA"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" width="45" height="45">
|
||||
<title>Checkmate</title>
|
||||
<desc>King silhouette adapted from Cburnett's chess set (Wikimedia Commons, CC BY-SA 3.0).</desc>
|
||||
<g fill="none" stroke="#13715B" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||
<path stroke-linejoin="miter" d="M22.5 11.63V6M20 8h5"/>
|
||||
<path fill="#fff" stroke-linecap="butt" stroke-linejoin="miter" d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5"/>
|
||||
<path fill="#fff" d="M12.5 37c5.5 3.5 14.5 3.5 20 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-2.5-7.5-12-10.5-16-4-3 6 6 10.5 6 10.5v7"/>
|
||||
<path d="M12.5 30c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0m-20 3.5c5.5-3 14.5-3 20 0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 819 B |
+21
-2
@@ -33,6 +33,14 @@ function App() {
|
||||
body: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
".Toastify__progress-bar": {
|
||||
height: 2,
|
||||
bottom: 0,
|
||||
top: "auto",
|
||||
},
|
||||
".Toastify__progress-bar--bg": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AppLayout>
|
||||
@@ -41,12 +49,23 @@ function App() {
|
||||
<ToastContainer
|
||||
newestOnTop={true}
|
||||
theme={mode}
|
||||
icon={false}
|
||||
style={
|
||||
{
|
||||
"--toastify-color-progress-light": "#7C8BA1",
|
||||
"--toastify-color-progress-dark": "#7C8BA1",
|
||||
"--toastify-color-progress-light": theme.palette.primary.main,
|
||||
"--toastify-color-progress-dark": theme.palette.primary.main,
|
||||
"--toastify-toast-min-height": "40px",
|
||||
"--toastify-toast-padding": "8px 12px",
|
||||
"--toastify-toast-width": "auto",
|
||||
} as CSSProperties
|
||||
}
|
||||
toastStyle={{
|
||||
minHeight: 40,
|
||||
padding: "8px 12px",
|
||||
width: "auto",
|
||||
maxWidth: "min(560px, calc(100vw - 32px))",
|
||||
}}
|
||||
progressStyle={{ height: 2 }}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</SWRConfig>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getResponseColor } from "@/Utils/DataUtils";
|
||||
import { HeatmapResponseTimeTooltip } from "@/Components/common/charts/HeatmapResponseTimeTooltip";
|
||||
import type { SxProps } from "@mui/material/styles";
|
||||
import type { ResponsiveStyleValue } from "@mui/system";
|
||||
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
interface PlaceholderCheck {
|
||||
status: "placeholder";
|
||||
responseTime: 0;
|
||||
@@ -32,11 +32,11 @@ export const HeatmapResponseTime = ({
|
||||
|
||||
if (!checks || checks.length === 0) return null;
|
||||
|
||||
const latestChecks = checks.slice(-25);
|
||||
const latestChecks = checks.slice(-MAX_RECENT_CHECKS);
|
||||
|
||||
let data: Array<CheckSnapshot | PlaceholderCheck>;
|
||||
if (latestChecks.length !== 25) {
|
||||
const placeholders = Array(25 - latestChecks.length).fill({
|
||||
if (latestChecks.length !== MAX_RECENT_CHECKS) {
|
||||
const placeholders = Array(MAX_RECENT_CHECKS - latestChecks.length).fill({
|
||||
status: "placeholder" as const,
|
||||
});
|
||||
data = [...latestChecks, ...placeholders];
|
||||
@@ -44,13 +44,13 @@ export const HeatmapResponseTime = ({
|
||||
data = latestChecks;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Box width={"100%"}>
|
||||
<Box
|
||||
width={"100%"}
|
||||
display={"grid"}
|
||||
gap={gap}
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(25, 1fr)",
|
||||
gap: gap,
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
alignItems: "stretch",
|
||||
}}
|
||||
>
|
||||
@@ -82,50 +82,50 @@ export const HeatmapResponseTime = ({
|
||||
check={check}
|
||||
>
|
||||
<Box
|
||||
display={"grid"}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateRows: "auto auto",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display={"flex"}
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={"100%"}
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
aspectRatio: "10",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
bgcolor={statusBg}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
bgcolor: statusBg,
|
||||
borderRadius: theme.spacing(0.5),
|
||||
...availabilityCellSx,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
pointerEvents: "none",
|
||||
borderRadius: theme.spacing(0.5),
|
||||
boxShadow: `inset 0 0 0 2px ${statusBorder}`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
width={"100%"}
|
||||
bgcolor={respBg}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
width: "100%",
|
||||
aspectRatio: "1 / 1",
|
||||
bgcolor: respBg,
|
||||
borderRadius: theme.spacing(0.5),
|
||||
...responseCellSx,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { CheckSnapshot } from "@/Types/Check";
|
||||
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
|
||||
interface HistogramResponseTimeProps {
|
||||
checks: CheckSnapshot[];
|
||||
@@ -34,7 +35,7 @@ const calculateResponseTimeStats = (checks: CheckSnapshot[]): ResponseTimeStats
|
||||
}
|
||||
|
||||
const responseTimes = validChecks.map((check) => check.originalResponseTime);
|
||||
const max = Math.max(...responseTimes);
|
||||
const max = Math.round(Math.max(...responseTimes));
|
||||
const avg = Math.round(
|
||||
responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
|
||||
);
|
||||
@@ -57,8 +58,11 @@ export const HistogramResponseTime = ({
|
||||
if (!Array.isArray(checks) || checks.length === 0) return null;
|
||||
|
||||
const data =
|
||||
checks.length !== 25
|
||||
? [...checks, ...Array(25 - checks.length).fill({ status: "placeholder" })]
|
||||
checks.length !== MAX_RECENT_CHECKS
|
||||
? [
|
||||
...checks,
|
||||
...Array(MAX_RECENT_CHECKS - checks.length).fill({ status: "placeholder" }),
|
||||
]
|
||||
: checks;
|
||||
|
||||
const chartHeight = typeof height === "number" ? `${height}px` : height;
|
||||
@@ -69,14 +73,20 @@ export const HistogramResponseTime = ({
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems={statsPosition === "left" ? "flex-end" : "flex-start"}
|
||||
minWidth={70}
|
||||
width={110}
|
||||
pr={statsPosition === "left" ? theme.spacing(8) : 0}
|
||||
pl={statsPosition === "right" ? theme.spacing(8) : 0}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
<Typography
|
||||
variant="body2"
|
||||
noWrap
|
||||
>
|
||||
{t("common.charts.histogram.avg", { value: stats.avg })}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<Typography
|
||||
variant="body2"
|
||||
noWrap
|
||||
>
|
||||
{t("common.charts.histogram.max", { value: stats.max })}
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -86,17 +96,17 @@ export const HistogramResponseTime = ({
|
||||
<Stack
|
||||
direction={statsPosition === "left" ? "row-reverse" : "row"}
|
||||
alignItems="center"
|
||||
sx={{ width: "100%" }}
|
||||
width={"100%"}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box
|
||||
width={"100%"}
|
||||
display={"grid"}
|
||||
gap={gridGap}
|
||||
height={chartHeight}
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(25, 1fr)",
|
||||
gap: gridGap,
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
alignItems: "end",
|
||||
height: chartHeight,
|
||||
}}
|
||||
>
|
||||
{data.map((check, index) => {
|
||||
@@ -108,23 +118,23 @@ export const HistogramResponseTime = ({
|
||||
: theme.palette.error.light;
|
||||
const bar = (
|
||||
<Box
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.action.hover}
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: theme.spacing(1),
|
||||
bgcolor: theme.palette.action.hover,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={"100%"}
|
||||
height={isPlaceholder ? 0 : heightPct}
|
||||
bgcolor={barColor}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: isPlaceholder ? 0 : heightPct,
|
||||
bgcolor: barColor,
|
||||
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
/>
|
||||
@@ -133,8 +143,8 @@ export const HistogramResponseTime = ({
|
||||
|
||||
return isPlaceholder ? (
|
||||
<Box
|
||||
height={"100%"}
|
||||
key={index}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
{bar}
|
||||
</Box>
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ResponsiveStyleValue } from "@mui/system";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
type BaseChartProps = React.PropsWithChildren<{
|
||||
icon: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
width?: number | string;
|
||||
maxWidth?: number | string;
|
||||
@@ -17,7 +17,6 @@ type BaseChartProps = React.PropsWithChildren<{
|
||||
|
||||
export const BaseChart = ({
|
||||
children,
|
||||
icon,
|
||||
title,
|
||||
width = "100%",
|
||||
maxWidth = "100%",
|
||||
@@ -38,34 +37,12 @@ export const BaseChart = ({
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
flex={1}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color="text.secondary"
|
||||
>
|
||||
{icon && (
|
||||
<BaseBox
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 34,
|
||||
height: 34,
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
"& svg": {
|
||||
width: 20,
|
||||
height: 20,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</BaseBox>
|
||||
)}
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
</Stack>
|
||||
{title}
|
||||
</Typography>
|
||||
<Box flex={1}>{children}</Box>
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import Logo from "@/assets/icons/checkmate-icon.svg?react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Link from "@mui/material/Link";
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import {
|
||||
ErrorFallback,
|
||||
EmptyFallback,
|
||||
EmptyMonitorFallback,
|
||||
BaseFallback,
|
||||
} from "@/Components/design-elements/Fallback";
|
||||
import { EmptyState } from "@/Components/design-elements/EmptyState";
|
||||
import { NoticeBanner } from "@/Components/design-elements/NoticeBanner";
|
||||
import { Breadcrumb } from "@/Components/design-elements/Breadcrumb";
|
||||
import { PageHeader } from "@/Components/design-elements/PageHeader";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { HeaderAuthControls } from "@/Pages/Auth/components/HeaderAuthControls";
|
||||
import { LanguageSelector, SwitchTheme } from "@/Components/inputs";
|
||||
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
@@ -21,16 +22,15 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
export const PageSpeedKeyPriorityFallback = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<BaseFallback>
|
||||
<Alert
|
||||
severity="warning"
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 600,
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
<EmptyState
|
||||
fullscreen
|
||||
title={t("pages.pageSpeed.fallback.title")}
|
||||
description={t("pages.pageSpeed.fallback.description")}
|
||||
alert={
|
||||
<NoticeBanner severity="warning">
|
||||
<Trans
|
||||
i18nKey="common.alerts.pageSpeedApiKey.content"
|
||||
components={{
|
||||
@@ -38,15 +38,18 @@ export const PageSpeedKeyPriorityFallback = () => {
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/settings"
|
||||
color="inherit"
|
||||
fontWeight="inherit"
|
||||
color={theme.palette.primary.main}
|
||||
fontWeight={600}
|
||||
sx={{
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Alert>
|
||||
</BaseFallback>
|
||||
</NoticeBanner>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,6 +58,9 @@ interface BasePageProps extends StackProps {
|
||||
error?: boolean;
|
||||
children: React.ReactNode;
|
||||
breadcrumbOverride?: string[];
|
||||
headerKey?: string;
|
||||
backTo?: string;
|
||||
backLabel?: string;
|
||||
}
|
||||
|
||||
export const BasePage = ({
|
||||
@@ -62,9 +68,13 @@ export const BasePage = ({
|
||||
error,
|
||||
children,
|
||||
breadcrumbOverride,
|
||||
headerKey,
|
||||
backTo,
|
||||
backLabel,
|
||||
...props
|
||||
}: BasePageProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -95,7 +105,16 @@ export const BasePage = ({
|
||||
spacing={theme.spacing(LAYOUT.LG)}
|
||||
{...props}
|
||||
>
|
||||
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
||||
{headerKey ? (
|
||||
<PageHeader
|
||||
title={t(`pages.${headerKey}.header.title`)}
|
||||
description={t(`pages.${headerKey}.header.description`)}
|
||||
backTo={backTo}
|
||||
backLabel={backLabel}
|
||||
/>
|
||||
) : (
|
||||
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
||||
)}
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
@@ -105,11 +124,12 @@ interface BasePageWithStatesProps extends StackProps {
|
||||
loading: boolean;
|
||||
error: any;
|
||||
totalCount: number;
|
||||
bullets: string[] | unknown;
|
||||
description?: string;
|
||||
page: string;
|
||||
actionButtonText: string;
|
||||
actionLink: string;
|
||||
children: React.ReactNode;
|
||||
headerKey?: string;
|
||||
}
|
||||
|
||||
export const BasePageWithStates = ({
|
||||
@@ -117,10 +137,11 @@ export const BasePageWithStates = ({
|
||||
error,
|
||||
totalCount,
|
||||
page,
|
||||
bullets,
|
||||
description,
|
||||
actionButtonText,
|
||||
actionLink,
|
||||
children,
|
||||
headerKey,
|
||||
...props
|
||||
}: BasePageWithStatesProps) => {
|
||||
const showLoading = loading && totalCount === 0;
|
||||
@@ -128,7 +149,7 @@ export const BasePageWithStates = ({
|
||||
if (!loading && totalCount === 0) {
|
||||
return (
|
||||
<EmptyFallback
|
||||
bullets={bullets}
|
||||
description={description}
|
||||
title={page}
|
||||
actionButtonText={actionButtonText}
|
||||
actionLink={actionLink}
|
||||
@@ -140,6 +161,7 @@ export const BasePageWithStates = ({
|
||||
<BasePage
|
||||
loading={showLoading}
|
||||
error={error}
|
||||
headerKey={headerKey}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -155,6 +177,7 @@ interface MonitorBasePageWithStatesProps extends StackProps {
|
||||
actionLink?: string;
|
||||
children: React.ReactNode;
|
||||
priorityFallback?: React.ReactNode;
|
||||
headerKey?: string;
|
||||
}
|
||||
|
||||
export const MonitorBasePageWithStates = ({
|
||||
@@ -165,6 +188,7 @@ export const MonitorBasePageWithStates = ({
|
||||
actionLink,
|
||||
children,
|
||||
priorityFallback,
|
||||
headerKey,
|
||||
...props
|
||||
}: MonitorBasePageWithStatesProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -172,15 +196,7 @@ export const MonitorBasePageWithStates = ({
|
||||
const showLoading = loading && totalCount === 0;
|
||||
|
||||
if (priorityFallback) {
|
||||
return (
|
||||
<BasePage
|
||||
loading={loading}
|
||||
error={error}
|
||||
{...props}
|
||||
>
|
||||
<Stack height={"100%"}>{priorityFallback}</Stack>
|
||||
</BasePage>
|
||||
);
|
||||
return <>{priorityFallback}</>;
|
||||
}
|
||||
|
||||
if (!loading && totalCount === 0) {
|
||||
@@ -188,7 +204,7 @@ export const MonitorBasePageWithStates = ({
|
||||
<EmptyMonitorFallback
|
||||
page={page}
|
||||
title={t(`pages.${page}.fallback.title`)}
|
||||
bullets={t(`pages.${page}.fallback.checks`, { returnObjects: true })}
|
||||
description={t(`pages.${page}.fallback.description`)}
|
||||
actionButtonText={t(`pages.${page}.fallback.actionButton`)}
|
||||
actionLink={actionLink || ""}
|
||||
/>
|
||||
@@ -199,6 +215,7 @@ export const MonitorBasePageWithStates = ({
|
||||
<BasePage
|
||||
loading={showLoading}
|
||||
error={error}
|
||||
headerKey={headerKey}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -215,51 +232,163 @@ export const BaseAuthPage = ({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
...props
|
||||
component,
|
||||
onSubmit,
|
||||
}: BaseAuthPageProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<BasePage
|
||||
breadcrumbOverride={[]}
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
minHeight="100vh"
|
||||
position={"relative"}
|
||||
{...props}
|
||||
width="100%"
|
||||
position="relative"
|
||||
sx={{ backgroundColor: theme.palette.background.default }}
|
||||
>
|
||||
<HeaderAuthControls
|
||||
hideLogo
|
||||
py={theme.spacing(LAYOUT.XS)}
|
||||
position={"absolute"}
|
||||
top={0}
|
||||
left={0}
|
||||
/>
|
||||
<Box width={{ xs: 60, sm: 70, md: 80 }}>
|
||||
<Logo
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
/>
|
||||
</Box>
|
||||
<Stack alignItems={"center"}>
|
||||
<Typography
|
||||
variant="h1"
|
||||
mb={theme.spacing(SPACING.LG)}
|
||||
<Stack
|
||||
flex={1}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
px={theme.spacing(LAYOUT.MD)}
|
||||
py={{ xs: theme.spacing(20), md: theme.spacing(12) }}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(2)}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: theme.spacing(LAYOUT.MD),
|
||||
right: theme.spacing(LAYOUT.MD),
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h2">{subtitle}</Typography>
|
||||
<LanguageSelector />
|
||||
<SwitchTheme />
|
||||
</Stack>
|
||||
<Box
|
||||
component={component as React.ElementType}
|
||||
onSubmit={onSubmit}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(LAYOUT.MD),
|
||||
width: "100%",
|
||||
maxWidth: 360,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(3)}
|
||||
mb={theme.spacing(2)}
|
||||
>
|
||||
<Box width={28}>
|
||||
<Logo width="100%" />
|
||||
</Box>
|
||||
<Typography sx={{ fontWeight: 500, letterSpacing: "-0.01em", fontSize: 16 }}>
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 26,
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.15,
|
||||
letterSpacing: "-0.02em",
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{children}
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
width={{
|
||||
xs: "80%",
|
||||
md: "25%",
|
||||
lg: "15%",
|
||||
flex={1}
|
||||
justifyContent="space-between"
|
||||
display={{ xs: "none", md: "flex" }}
|
||||
sx={{
|
||||
background: "linear-gradient(135deg, #0c4d3d 0%, #155a48 60%, #0f5d4a 100%)",
|
||||
color: "#fff",
|
||||
p: theme.spacing(28),
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundImage: `
|
||||
linear-gradient(45deg, rgba(255,255,255,0.04) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, rgba(255,255,255,0.04) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.04) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.04) 75%)
|
||||
`,
|
||||
backgroundSize: "80px 80px",
|
||||
backgroundPosition: "0 0, 0 40px, 40px -40px, -40px 0px",
|
||||
maskImage: "linear-gradient(135deg, transparent 0%, black 70%)",
|
||||
WebkitMaskImage: "linear-gradient(135deg, transparent 0%, black 70%)",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<div />
|
||||
<Stack sx={{ position: "relative", zIndex: 1, gap: theme.spacing(4) }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.12em",
|
||||
color: "rgba(255,255,255,0.75)",
|
||||
}}
|
||||
>
|
||||
{t("pages.auth.brandPanel.eyebrow")}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 44,
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: "-0.02em",
|
||||
maxWidth: 460,
|
||||
}}
|
||||
>
|
||||
{t("pages.auth.brandPanel.tagline")}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 17,
|
||||
lineHeight: 1.5,
|
||||
color: "rgba(255,255,255,0.75)",
|
||||
maxWidth: 460,
|
||||
}}
|
||||
>
|
||||
{t("pages.auth.brandPanel.description")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography
|
||||
sx={{
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
v{__APP_VERSION__}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</BasePage>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Button } from "@/Components/inputs";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { EmptyStateIllustration } from "./EmptyStateIllustration";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
actionText?: string;
|
||||
actionTo?: string;
|
||||
onAction?: () => void;
|
||||
secondaryText?: string;
|
||||
secondaryHref?: string;
|
||||
compact?: boolean;
|
||||
fullscreen?: boolean;
|
||||
alert?: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const EmptyState = ({
|
||||
title,
|
||||
description,
|
||||
actionText,
|
||||
actionTo,
|
||||
onAction,
|
||||
secondaryText,
|
||||
secondaryHref,
|
||||
compact = false,
|
||||
fullscreen = false,
|
||||
alert,
|
||||
children,
|
||||
}: EmptyStateProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
sx={{ py: theme.spacing(LAYOUT.MD), px: theme.spacing(LAYOUT.SM) }}
|
||||
>
|
||||
<EmptyStateIllustration
|
||||
width={80}
|
||||
height={60}
|
||||
/>
|
||||
<Stack sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
...(fullscreen && { minHeight: "65vh" }),
|
||||
py: theme.spacing(LAYOUT.XXL),
|
||||
px: theme.spacing(LAYOUT.MD),
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<EmptyStateIllustration />
|
||||
<Typography
|
||||
sx={{
|
||||
mt: theme.spacing(LAYOUT.MD),
|
||||
mb: theme.spacing(LAYOUT.XS),
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
sx={{
|
||||
maxWidth: 360,
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.55,
|
||||
mb: theme.spacing(LAYOUT.LG),
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
{alert && (
|
||||
<Stack sx={{ width: "100%", maxWidth: 520, mb: theme.spacing(LAYOUT.MD) }}>
|
||||
{alert}
|
||||
</Stack>
|
||||
)}
|
||||
{(actionText || secondaryText) && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
{actionText && (actionTo || onAction) && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
{...(actionTo
|
||||
? { component: RouterLink, to: actionTo }
|
||||
: { onClick: onAction })}
|
||||
>
|
||||
{actionText}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryText && secondaryHref && (
|
||||
<Typography
|
||||
component="a"
|
||||
href={secondaryHref}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: "none",
|
||||
"&:hover": { textDecoration: "underline" },
|
||||
}}
|
||||
>
|
||||
{secondaryText} →
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
interface EmptyStateIllustrationProps {
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
const PALETTE = {
|
||||
light: {
|
||||
grid: "#CBD5E1",
|
||||
backFill: "#F5F7FA",
|
||||
backStroke: "#D5DBE3",
|
||||
backInner: "#B6C2CF",
|
||||
frontFill: "#EAF5F0",
|
||||
frontStroke: "#7DBFAA",
|
||||
frontInner: "#4DAF94",
|
||||
},
|
||||
dark: {
|
||||
grid: "#3A4452",
|
||||
backFill: "#475569",
|
||||
backStroke: "#64748B",
|
||||
backInner: "#CBD5E1",
|
||||
frontFill: "#0F5D4A",
|
||||
frontStroke: "#4DAF94",
|
||||
frontInner: "#B5E2D2",
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyStateIllustration = ({
|
||||
width = 160,
|
||||
height = 120,
|
||||
}: EmptyStateIllustrationProps) => {
|
||||
const theme = useTheme();
|
||||
const p = theme.palette.mode === "dark" ? PALETTE.dark : PALETTE.light;
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 160 120"
|
||||
width={width}
|
||||
height={height}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g
|
||||
stroke={p.grid}
|
||||
strokeWidth="0.75"
|
||||
>
|
||||
<line
|
||||
x1="16"
|
||||
y1="0"
|
||||
x2="16"
|
||||
y2="120"
|
||||
/>
|
||||
<line
|
||||
x1="56"
|
||||
y1="0"
|
||||
x2="56"
|
||||
y2="120"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<line
|
||||
x1="104"
|
||||
y1="0"
|
||||
x2="104"
|
||||
y2="120"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<line
|
||||
x1="144"
|
||||
y1="0"
|
||||
x2="144"
|
||||
y2="120"
|
||||
/>
|
||||
<line
|
||||
x1="0"
|
||||
y1="12"
|
||||
x2="160"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="0"
|
||||
y1="44"
|
||||
x2="160"
|
||||
y2="44"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<line
|
||||
x1="0"
|
||||
y1="76"
|
||||
x2="160"
|
||||
y2="76"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<line
|
||||
x1="0"
|
||||
y1="108"
|
||||
x2="160"
|
||||
y2="108"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<g>
|
||||
<rect
|
||||
x="38"
|
||||
y="22"
|
||||
width="92"
|
||||
height="42"
|
||||
rx="6"
|
||||
fill={p.backFill}
|
||||
stroke={p.backStroke}
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="48"
|
||||
y="32"
|
||||
width="14"
|
||||
height="22"
|
||||
rx="2"
|
||||
fill={p.backInner}
|
||||
opacity="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="68"
|
||||
y="34"
|
||||
width="50"
|
||||
height="2.5"
|
||||
rx="1.25"
|
||||
fill={p.backInner}
|
||||
opacity="0.7"
|
||||
/>
|
||||
<rect
|
||||
x="68"
|
||||
y="42"
|
||||
width="38"
|
||||
height="2.5"
|
||||
rx="1.25"
|
||||
fill={p.backInner}
|
||||
opacity="0.55"
|
||||
/>
|
||||
<rect
|
||||
x="68"
|
||||
y="50"
|
||||
width="44"
|
||||
height="2.5"
|
||||
rx="1.25"
|
||||
fill={p.backInner}
|
||||
opacity="0.55"
|
||||
/>
|
||||
<rect
|
||||
x="22"
|
||||
y="50"
|
||||
width="92"
|
||||
height="42"
|
||||
rx="6"
|
||||
fill={p.frontFill}
|
||||
stroke={p.frontStroke}
|
||||
strokeWidth="0.5"
|
||||
strokeOpacity="0.4"
|
||||
/>
|
||||
<rect
|
||||
x="32"
|
||||
y="60"
|
||||
width="14"
|
||||
height="22"
|
||||
rx="2"
|
||||
fill={p.frontInner}
|
||||
opacity="0.35"
|
||||
/>
|
||||
<rect
|
||||
x="52"
|
||||
y="62"
|
||||
width="50"
|
||||
height="2.5"
|
||||
rx="1.25"
|
||||
fill={p.frontInner}
|
||||
opacity="0.55"
|
||||
/>
|
||||
<rect
|
||||
x="52"
|
||||
y="70"
|
||||
width="38"
|
||||
height="2.5"
|
||||
rx="1.25"
|
||||
fill={p.frontInner}
|
||||
opacity="0.4"
|
||||
/>
|
||||
<rect
|
||||
x="52"
|
||||
y="78"
|
||||
width="44"
|
||||
height="2.5"
|
||||
rx="1.25"
|
||||
fill={p.frontInner}
|
||||
opacity="0.4"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,8 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements";
|
||||
import { Button } from "@/Components/inputs";
|
||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
import { useNavigate } from "react-router";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { EmptyState } from "./EmptyState";
|
||||
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
|
||||
interface BaseFallbackProps extends StackProps {
|
||||
@@ -15,30 +11,13 @@ interface BaseFallbackProps extends StackProps {
|
||||
|
||||
export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
return (
|
||||
<Stack
|
||||
alignItems={"center"}
|
||||
margin={isSmall ? "inherit" : "auto"}
|
||||
marginTop={isSmall ? "33%" : "10%"}
|
||||
width={{
|
||||
sm: "90%",
|
||||
md: "70%",
|
||||
lg: "50%",
|
||||
xl: "40%",
|
||||
}}
|
||||
padding={{ xs: theme.spacing(LAYOUT.MD), md: theme.spacing(LAYOUT.XXL) }}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
border={1}
|
||||
borderColor={theme.palette.divider}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
borderStyle: "dashed",
|
||||
}}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{ width: "100%", py: theme.spacing(LAYOUT.LG) }}
|
||||
{...props}
|
||||
>
|
||||
<SkeletonCard showHalo={true} />
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
@@ -51,126 +30,56 @@ export const ErrorFallback = ({
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(LAYOUT.XS)}
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography>{subtitle}</Typography>
|
||||
</BaseFallback>
|
||||
<EmptyState
|
||||
fullscreen
|
||||
title={title}
|
||||
description={subtitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyFallback = ({
|
||||
title,
|
||||
bullets,
|
||||
description,
|
||||
actionButtonText,
|
||||
actionLink,
|
||||
}: {
|
||||
title: string;
|
||||
bullets: any;
|
||||
description?: string;
|
||||
actionButtonText: string;
|
||||
actionLink: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<BaseFallback>
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
zIndex={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography component="h1">{title}</Typography>
|
||||
<Stack
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
gap: theme.spacing(SPACING.MD),
|
||||
maxWidth: "1100px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{Array.isArray(bullets) &&
|
||||
bullets?.map((bullet: string) => (
|
||||
<BulletPointCheck
|
||||
text={bullet}
|
||||
key={`${bullet}-${Math.random()}`}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(actionLink)}
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BaseFallback>
|
||||
<EmptyState
|
||||
fullscreen
|
||||
title={title}
|
||||
description={description}
|
||||
actionText={actionButtonText}
|
||||
actionTo={actionLink}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyMonitorFallback = ({
|
||||
page,
|
||||
title,
|
||||
bullets,
|
||||
description,
|
||||
actionButtonText,
|
||||
actionLink,
|
||||
}: {
|
||||
page: string;
|
||||
title: string;
|
||||
bullets: any;
|
||||
description?: string;
|
||||
actionButtonText: string;
|
||||
actionLink: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<BaseFallback>
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
zIndex={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
gap: theme.spacing(SPACING.MD),
|
||||
maxWidth: "1100px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{Array.isArray(bullets) &&
|
||||
bullets?.map((bullet: string, index: number) => (
|
||||
<BulletPointCheck
|
||||
text={bullet}
|
||||
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(actionLink)}
|
||||
>
|
||||
{actionButtonText}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BaseFallback>
|
||||
<EmptyState
|
||||
fullscreen
|
||||
title={title}
|
||||
description={description}
|
||||
actionText={actionButtonText}
|
||||
actionTo={actionLink}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +1,36 @@
|
||||
import { BaseChart } from "@/Components/design-elements";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Box, { type BoxProps } from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { forwardRef, useMemo, useState, useEffect } from "react";
|
||||
import { getInfraGaugeColor } from "@/Utils/MonitorUtils";
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
|
||||
export const Gauge = ({
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
}: {
|
||||
interface GaugeProps extends Omit<BoxProps, "color"> {
|
||||
isLoading?: boolean;
|
||||
progress?: number;
|
||||
radius?: number;
|
||||
strokeWidth?: number;
|
||||
precision?: number;
|
||||
unit?: string;
|
||||
}) => {
|
||||
}
|
||||
|
||||
export const Gauge = forwardRef<HTMLDivElement, GaugeProps>(function Gauge(
|
||||
{
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const theme = useTheme();
|
||||
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
|
||||
|
||||
@@ -51,17 +57,19 @@ export const Gauge = ({
|
||||
const fillColor = getInfraGaugeColor(progressWithinRange, theme);
|
||||
|
||||
if (isLoading) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
display={"inline-block"}
|
||||
position={"relative"}
|
||||
width={radius}
|
||||
height={radius}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
borderRadius={"50%"}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
viewBox={`0 0 ${totalSize} ${totalSize}`}
|
||||
@@ -105,7 +113,7 @@ export const Gauge = ({
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const DetailGauge = ({
|
||||
title,
|
||||
@@ -114,6 +122,7 @@ export const DetailGauge = ({
|
||||
upperValue,
|
||||
lowerLabel,
|
||||
lowerValue,
|
||||
maxWidth = 225,
|
||||
}: {
|
||||
title: string;
|
||||
progress: number;
|
||||
@@ -121,13 +130,14 @@ export const DetailGauge = ({
|
||||
upperValue?: string | number;
|
||||
lowerLabel?: string;
|
||||
lowerValue?: string | number;
|
||||
maxWidth?: number;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<BaseChart
|
||||
icon={null}
|
||||
title={title}
|
||||
maxWidth={225}
|
||||
maxWidth={maxWidth}
|
||||
>
|
||||
<Stack
|
||||
alignItems={"center"}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { PulseDot, Dot } from "@/Components/design-elements";
|
||||
import { PulseDot, Dot, StrategyBadge } from "@/Components/design-elements";
|
||||
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
@@ -17,7 +18,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
return (
|
||||
<Stack>
|
||||
<Typography
|
||||
fontSize={typographyLevels.xl}
|
||||
fontSize={typographyLevels.xxl}
|
||||
fontWeight={500}
|
||||
overflow={"hidden"}
|
||||
textOverflow={"ellipsis"}
|
||||
@@ -28,19 +29,25 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<PulseDot color={getStatusColor(monitor.status, theme)} />
|
||||
<Typography
|
||||
fontSize={typographyLevels.l}
|
||||
fontWeight={"bolder"}
|
||||
fontFamily={"monospace"}
|
||||
fontFamily={theme.typography.fontFamilyMonospace}
|
||||
overflow={"hidden"}
|
||||
textOverflow={"ellipsis"}
|
||||
whiteSpace={"nowrap"}
|
||||
>
|
||||
{formatUrl(monitor?.url)}
|
||||
</Typography>
|
||||
{monitor.type === "pagespeed" && monitor.strategy && (
|
||||
<>
|
||||
<Dot />
|
||||
<StrategyBadge strategy={monitor.strategy} />
|
||||
</>
|
||||
)}
|
||||
{!isSmall && (
|
||||
<>
|
||||
<Dot />
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme, alpha } from "@mui/material/styles";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
|
||||
export const Severities = ["info", "warning", "error"] as const;
|
||||
export type Severity = (typeof Severities)[number];
|
||||
|
||||
const SEVERITY_GLYPH: Record<Severity, string> = {
|
||||
info: "ⓘ",
|
||||
warning: "⚠",
|
||||
error: "✕",
|
||||
};
|
||||
|
||||
interface NoticeBannerProps {
|
||||
severity?: Severity;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NoticeBanner = ({ severity = "info", children }: NoticeBannerProps) => {
|
||||
const theme = useTheme();
|
||||
const tone = theme.palette[severity].main;
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
width={"100%"}
|
||||
p={theme.spacing(LAYOUT.MD)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
border={`1px solid ${alpha(tone, 0.45)}`}
|
||||
bgcolor={alpha(tone, 0.08)}
|
||||
textAlign={"left"}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
color={tone}
|
||||
fontSize={typographyLevels.xl}
|
||||
lineHeight={1}
|
||||
mt={LAYOUT.XXS}
|
||||
aria-hidden
|
||||
>
|
||||
{SEVERITY_GLYPH[severity]}
|
||||
</Box>
|
||||
<Typography
|
||||
color={theme.palette.text.primary}
|
||||
lineHeight={1.55}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { ChevronLeft, HelpCircle, FileText, Code } from "lucide-react";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const EXTERNAL_LINKS = {
|
||||
support: "https://discord.com/invite/NAb6H3UTjK",
|
||||
docs: "https://checkmate.so/docs",
|
||||
changelog: "https://github.com/bluewave-labs/checkmate/releases",
|
||||
};
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
backTo?: string;
|
||||
backLabel?: string;
|
||||
}
|
||||
|
||||
export const PageHeader = ({
|
||||
title,
|
||||
description,
|
||||
backTo,
|
||||
backLabel,
|
||||
}: PageHeaderProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const linkItems: { key: keyof typeof EXTERNAL_LINKS; icon: ReactNode }[] = [
|
||||
{
|
||||
key: "support",
|
||||
icon: (
|
||||
<HelpCircle
|
||||
size={16}
|
||||
strokeWidth={1.6}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "docs",
|
||||
icon: (
|
||||
<FileText
|
||||
size={16}
|
||||
strokeWidth={1.6}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "changelog",
|
||||
icon: (
|
||||
<Code
|
||||
size={16}
|
||||
strokeWidth={1.6}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
justifyContent="space-between"
|
||||
gap={6}
|
||||
sx={{ mb: theme.spacing(8) }}
|
||||
>
|
||||
<Stack gap={theme.spacing(1)}>
|
||||
{backTo && backLabel && (
|
||||
<Box
|
||||
component={RouterLink}
|
||||
to={backTo}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
textDecoration: "none",
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 13,
|
||||
mb: theme.spacing(1),
|
||||
"&:hover": { color: theme.palette.text.primary },
|
||||
}}
|
||||
>
|
||||
<ChevronLeft
|
||||
size={14}
|
||||
strokeWidth={1.8}
|
||||
/>
|
||||
{backLabel}
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 26,
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.primary,
|
||||
lineHeight: 1.15,
|
||||
letterSpacing: "-0.02em",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.55,
|
||||
maxWidth: 640,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 1,
|
||||
overflow: "hidden",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{linkItems.map((item, idx) => (
|
||||
<Tooltip
|
||||
key={item.key}
|
||||
title={t(`components.pageHeader.links.${item.key}`)}
|
||||
>
|
||||
<Box
|
||||
component="a"
|
||||
href={EXTERNAL_LINKS[item.key]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 36,
|
||||
height: 32,
|
||||
color: theme.palette.text.secondary,
|
||||
borderRight:
|
||||
idx < linkItems.length - 1
|
||||
? `1px solid ${theme.palette.divider}`
|
||||
: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -53,7 +53,7 @@ export const ConfigBox = ({
|
||||
rightContent,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
subtitle: React.ReactNode;
|
||||
leftContent?: React.ReactNode;
|
||||
rightContent: React.ReactNode;
|
||||
}) => {
|
||||
@@ -63,9 +63,9 @@ export const ConfigBox = ({
|
||||
left={
|
||||
<Stack spacing={theme.spacing(LAYOUT.XS)}>
|
||||
<Typography
|
||||
textTransform={"capitalize"}
|
||||
component="h2"
|
||||
variant="h2"
|
||||
variant="eyebrow"
|
||||
color="text.secondary"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
@@ -69,7 +69,12 @@ export const StatBox = ({
|
||||
>
|
||||
<Stack onClick={onClick}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
|
||||
<Typography color={textColor}>{title}</Typography>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={palette ? textColor : "text.secondary"}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{tooltip && (
|
||||
<TooltipWithInfo
|
||||
title={tooltip}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
padding: theme.spacing(4),
|
||||
padding: theme.spacing(6),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
@@ -65,9 +65,13 @@ const StatusBox = ({
|
||||
<BGBox sx={sx}>
|
||||
<Stack spacing={theme.spacing(4)}>
|
||||
<Typography
|
||||
variant={"h2"}
|
||||
textTransform="uppercase"
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.08em",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
formatStatusCode,
|
||||
getStatusCodeTooltip,
|
||||
getStatusCodeValueType,
|
||||
} from "@/Utils/statusCode";
|
||||
import { ValueLabel } from "./StatusLabel";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
export type StatusCodeLabelProps = {
|
||||
statusCode: number | null | undefined;
|
||||
message?: string | null;
|
||||
fallback?: string;
|
||||
};
|
||||
|
||||
export const StatusCodeLabel = ({
|
||||
statusCode,
|
||||
message,
|
||||
fallback = "N/A",
|
||||
}: StatusCodeLabelProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (!statusCode) {
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
const text = formatStatusCode(statusCode, t);
|
||||
const tooltip = getStatusCodeTooltip(statusCode, message, t);
|
||||
const body = (
|
||||
<ValueLabel
|
||||
value={getStatusCodeValueType(statusCode)}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
if (!tooltip) return body;
|
||||
return (
|
||||
<Tooltip title={tooltip}>
|
||||
<span>{body}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -93,3 +93,20 @@ export const ValueLabel = ({ value, text }: { value: ValueType; text: string })
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColoredLabel = ({ text, color }: { text: string; color: string }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BaseBox
|
||||
sx={{
|
||||
width: "fit-content",
|
||||
padding: theme.spacing(1, 3),
|
||||
color: color,
|
||||
borderColor: color,
|
||||
}}
|
||||
>
|
||||
<Typography>{text}</Typography>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme, alpha } from "@mui/material/styles";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface StepProgressProps {
|
||||
steps: number;
|
||||
current: number;
|
||||
}
|
||||
|
||||
export const StepProgress = ({ steps, current }: StepProgressProps) => {
|
||||
const theme = useTheme();
|
||||
const activeBg = `linear-gradient(135deg, ${theme.palette.primary.light} 0%, ${theme.palette.primary.main} 100%)`;
|
||||
const completedBg = `linear-gradient(135deg, ${alpha(theme.palette.primary.light, 0.5)} 0%, ${alpha(theme.palette.primary.main, 0.5)} 100%)`;
|
||||
|
||||
const getSegmentBackground = (index: number) => {
|
||||
if (index < current) return completedBg;
|
||||
if (index === current) return activeBg;
|
||||
return theme.palette.divider;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
width="100%"
|
||||
>
|
||||
{Array.from({ length: steps }).map((_, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
flex={1}
|
||||
height={theme.spacing(LAYOUT.XXS)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
background: getSegmentBackground(index),
|
||||
transition: "background 0.2s ease",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { PageSpeedStrategy } from "@/Types/Monitor";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
interface StrategyBadgeProps {
|
||||
strategy: PageSpeedStrategy | undefined;
|
||||
variant?: "caption" | "body2";
|
||||
}
|
||||
|
||||
export const StrategyBadge = ({ strategy, variant = "body2" }: StrategyBadgeProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!strategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant={variant}
|
||||
px={SPACING.XXL}
|
||||
py={SPACING.SM}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.secondary.light}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{t(`pages.pageSpeed.strategy.${strategy}`)}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
@@ -18,10 +18,9 @@ import {
|
||||
ChevronsRight,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Coffee,
|
||||
Ellipsis,
|
||||
PartyPopper,
|
||||
} from "lucide-react";
|
||||
import { EmptyState } from "./EmptyState";
|
||||
|
||||
import TablePagination from "@mui/material/TablePagination";
|
||||
import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
|
||||
@@ -36,6 +35,9 @@ import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
export type Header<T> = {
|
||||
id: number | string;
|
||||
content: React.ReactNode;
|
||||
mobileLabel?: React.ReactNode;
|
||||
hideMobileLabel?: boolean;
|
||||
align?: "left" | "center" | "right" | "justify" | "inherit";
|
||||
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
|
||||
render: (row: T) => React.ReactNode;
|
||||
};
|
||||
@@ -110,6 +112,24 @@ export function DataTable<
|
||||
key={key}
|
||||
>
|
||||
{headers.map((header) => {
|
||||
if (header.hideMobileLabel) {
|
||||
return (
|
||||
<Grid2
|
||||
container
|
||||
key={header.id}
|
||||
>
|
||||
<Grid2
|
||||
size={12}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
{header.render(row)}
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid2
|
||||
container
|
||||
@@ -124,7 +144,7 @@ export function DataTable<
|
||||
component="div"
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{header.content}
|
||||
{header.mobileLabel ?? header.content}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2
|
||||
@@ -163,14 +183,19 @@ export function DataTable<
|
||||
},
|
||||
"& .MuiTableHead-root .MuiTableRow-root": {
|
||||
height: "28px",
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
"& .MuiTableHead-root .MuiTableCell-root": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"& :is(th)": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 500,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
padding: `${theme.spacing(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`,
|
||||
fontSize: theme.typography.fontSize,
|
||||
fontSize: 11,
|
||||
},
|
||||
"& :is(td)": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
@@ -189,7 +214,7 @@ export function DataTable<
|
||||
{headers.map((header, idx) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={idx === 0 ? "left" : "center"}
|
||||
align={header.align ?? (idx === 0 ? "left" : "center")}
|
||||
key={header.id}
|
||||
>
|
||||
{header.content}
|
||||
@@ -218,7 +243,7 @@ export function DataTable<
|
||||
{headers.map((header, index) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={index === 0 ? "left" : "center"}
|
||||
align={header.align ?? (index === 0 ? "left" : "center")}
|
||||
key={header.id}
|
||||
onClick={
|
||||
header.onClick ? (e) => header.onClick!(e, row) : undefined
|
||||
@@ -548,49 +573,18 @@ export const Pagination = ({ ...props }: PaginationProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) => {
|
||||
const theme = useTheme();
|
||||
const EmptyView = ({ text }: { text?: string; positive?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const Icon = positive ? PartyPopper : Coffee;
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
<Box
|
||||
sx={{
|
||||
py: theme.spacing(LAYOUT.XL),
|
||||
px: theme.spacing(LAYOUT.XS),
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.divider,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
textAlign: "center",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 1,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
mb: theme.spacing(SPACING.LG),
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={28}
|
||||
strokeWidth={1}
|
||||
color={theme.palette.text.secondary}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{text ?? t("common.table.empty")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<EmptyState title={text ?? t("common.table.empty")} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,11 @@ import type { TooltipProps } from "@mui/material/Tooltip";
|
||||
|
||||
type StyledTooltipProps = TooltipProps;
|
||||
|
||||
export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => {
|
||||
export const Tooltip = ({
|
||||
placement = "top",
|
||||
slotProps,
|
||||
...props
|
||||
}: StyledTooltipProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@@ -16,18 +20,18 @@ export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) =>
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
background:
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark"
|
||||
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)"
|
||||
: "linear-gradient(135deg, #2c3e50 0%, #1a252f 100%)",
|
||||
backgroundColor: "transparent",
|
||||
color: "#ffffff",
|
||||
? theme.palette.grey[900]
|
||||
: theme.palette.grey[800],
|
||||
color: theme.palette.common.white,
|
||||
fontSize: "13px",
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(5)}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.4)",
|
||||
boxShadow: theme.shadows[6],
|
||||
},
|
||||
},
|
||||
...slotProps,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export * from "./Breadcrumb";
|
||||
export * from "./PageHeader";
|
||||
export * from "./BasePage";
|
||||
export * from "./EmptyState";
|
||||
export * from "./EmptyStateIllustration";
|
||||
export * from "./Fallback";
|
||||
export * from "./SkeletonCard";
|
||||
export * from "./BulletPointCheck";
|
||||
@@ -19,5 +22,9 @@ export * from "./Gauge";
|
||||
export * from "./Tabs";
|
||||
export * from "./SplitBox";
|
||||
export * from "./TextLink";
|
||||
export * from "./NoticeBanner";
|
||||
export * from "./OfflineBanner";
|
||||
export * from "./Avatar";
|
||||
export * from "./StatusCodeLabel";
|
||||
export * from "./StrategyBadge";
|
||||
export * from "./StepProgress";
|
||||
@@ -14,17 +14,19 @@ type AutoCompleteInputProps = Omit<
|
||||
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
|
||||
fieldLabel?: string;
|
||||
required?: boolean;
|
||||
renderOptionContent?: (option: any) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const AutoCompleteInput = ({
|
||||
fieldLabel,
|
||||
required,
|
||||
renderInput,
|
||||
renderOptionContent,
|
||||
...props
|
||||
}: AutoCompleteInputProps) => {
|
||||
const theme = useTheme();
|
||||
const multiple = props.multiple;
|
||||
|
||||
const { renderOption: customRenderOption, ...autocompleteProps } = props;
|
||||
const defaultRenderInput = (params: any) => (
|
||||
<TextField
|
||||
{...params}
|
||||
@@ -34,7 +36,7 @@ export const AutoCompleteInput = ({
|
||||
|
||||
const autocomplete = (
|
||||
<Autocomplete
|
||||
{...props}
|
||||
{...autocompleteProps}
|
||||
disableCloseOnSelect={!!multiple}
|
||||
popupIcon={
|
||||
<ChevronDown
|
||||
@@ -46,24 +48,28 @@ export const AutoCompleteInput = ({
|
||||
renderInput={renderInput || defaultRenderInput}
|
||||
getOptionKey={(option) => option.id}
|
||||
renderTags={() => null}
|
||||
renderOption={(props, option, { selected }) => {
|
||||
const { key, ...optionProps } = props;
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
{...optionProps}
|
||||
>
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(2)}
|
||||
renderOption={
|
||||
customRenderOption ||
|
||||
((props, option, { selected }) => {
|
||||
const { key, ...optionProps } = props;
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
{...optionProps}
|
||||
>
|
||||
{multiple && <Checkbox checked={selected} />}
|
||||
{option.name}
|
||||
</Stack>
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(2)}
|
||||
width="100%"
|
||||
>
|
||||
{multiple && <Checkbox checked={selected} />}
|
||||
{renderOptionContent ? renderOptionContent(option) : option.name}
|
||||
</Stack>
|
||||
</ListItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
sx={{
|
||||
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
||||
padding: `0 ${theme.spacing(5)}`,
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import type { ButtonProps } from "@mui/material/Button";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTheme, darken } from "@mui/material/styles";
|
||||
import { HOVER } from "@/Utils/Theme/constants";
|
||||
|
||||
const PALETTE_COLORS = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"error",
|
||||
"warning",
|
||||
"info",
|
||||
"success",
|
||||
] as const;
|
||||
type PaletteColor = (typeof PALETTE_COLORS)[number];
|
||||
|
||||
export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const outlinedSx =
|
||||
|
||||
const hoverOverrides: Record<string, unknown> = { boxShadow: "none" };
|
||||
|
||||
if (props.variant === "outlined") {
|
||||
hoverOverrides.borderColor = theme.palette.text.secondary;
|
||||
}
|
||||
|
||||
if (props.variant === "contained") {
|
||||
const colorKey = (props.color ?? "primary") as PaletteColor;
|
||||
const palette = theme.palette[colorKey];
|
||||
if (palette && typeof palette === "object" && "main" in palette) {
|
||||
hoverOverrides.backgroundColor = darken(palette.main, HOVER.DARKEN);
|
||||
}
|
||||
}
|
||||
|
||||
const variantSx =
|
||||
props.variant === "outlined"
|
||||
? {
|
||||
color: theme.palette.text.primary,
|
||||
borderColor: theme.palette.divider,
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.text.secondary,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
@@ -26,10 +50,8 @@ export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
boxShadow: "none",
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
...outlinedSx,
|
||||
"&:hover": hoverOverrides,
|
||||
...variantSx,
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef } from "react";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import type { CheckboxProps } from "@mui/material/Checkbox";
|
||||
import { Square, SquareCheck } from "lucide-react";
|
||||
import { Square, SquareCheck, SquareMinus } from "lucide-react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
type CheckboxInputProps = CheckboxProps & {
|
||||
@@ -34,9 +34,15 @@ export const CheckboxInput = forwardRef<HTMLInputElement, CheckboxInputProps>(
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
indeterminateIcon={
|
||||
<SquareMinus
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
"&.Mui-checked": {
|
||||
"&.Mui-checked, &.MuiCheckbox-indeterminate": {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
"&:hover": { backgroundColor: "transparent" },
|
||||
|
||||
@@ -2,10 +2,12 @@ import Dialog from "@mui/material/Dialog";
|
||||
import type { DialogProps } from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Button } from "@/Components/inputs";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
@@ -16,7 +18,6 @@ export const DialogInput = ({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
confirmColor = "primary",
|
||||
cancelColor = "error",
|
||||
loading = false,
|
||||
cancelText,
|
||||
confirmText,
|
||||
@@ -31,7 +32,6 @@ export const DialogInput = ({
|
||||
onConfirm?(item: any): any;
|
||||
onCancel?(item: any): any;
|
||||
confirmColor?: "error" | "primary";
|
||||
cancelColor?: "error" | "primary";
|
||||
loading?: boolean;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
@@ -41,22 +41,58 @@ export const DialogInput = ({
|
||||
additionalButtons?: ReactNode;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={maxWidth}
|
||||
fullWidth={fullWidth}
|
||||
onClose={(_event, reason) => {
|
||||
if (reason !== "backdropClick" && onCancel) onCancel(undefined);
|
||||
}}
|
||||
>
|
||||
{title && <DialogTitle sx={{ fontSize: typographyLevels.l }}>{title}</DialogTitle>}
|
||||
<DialogContent>
|
||||
{content && <DialogContentText>{content}</DialogContentText>}
|
||||
{children}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{title && (
|
||||
<DialogTitle
|
||||
pb={theme.spacing(LAYOUT.XS)}
|
||||
bgcolor={theme.palette.action.hover}
|
||||
borderBottom={`1px solid ${theme.palette.divider}`}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h2"
|
||||
fontWeight={600}
|
||||
display={"block"}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{content && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={theme.palette.text.secondary}
|
||||
mt={theme.spacing(LAYOUT.XS)}
|
||||
display={"block"}
|
||||
>
|
||||
{content}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogTitle>
|
||||
)}
|
||||
{children && (
|
||||
<DialogContent sx={{ pt: theme.spacing(LAYOUT.MD) }}>
|
||||
<Box pt={theme.spacing(LAYOUT.XS)}>{children}</Box>
|
||||
</DialogContent>
|
||||
)}
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: theme.spacing(LAYOUT.MD),
|
||||
pt: theme.spacing(LAYOUT.SM),
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="contained"
|
||||
color={cancelColor}
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{cancelText ?? t("common.buttons.cancel")}
|
||||
|
||||
@@ -15,9 +15,9 @@ export const FieldLabel = ({ children, required = false, htmlFor }: FieldLabelPr
|
||||
return (
|
||||
<Typography
|
||||
component="label"
|
||||
variant="body1"
|
||||
htmlFor={htmlFor}
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(2),
|
||||
|
||||
@@ -126,7 +126,12 @@ export const ImageUpload = ({
|
||||
onChange={(e) => handleFile(e.target.files?.[0])}
|
||||
style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }}
|
||||
/>
|
||||
<Upload size={24} />
|
||||
<Upload
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
color="currentColor"
|
||||
style={{ opacity: 0.7 }}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { Select } from "@/Components/inputs";
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setLanguage } from "@/Features/UI/uiSlice";
|
||||
import type { RootState } from "@/Types/state";
|
||||
|
||||
const langMap: Record<string, string> = {
|
||||
cs: "cz",
|
||||
ja: "jp",
|
||||
uk: "ua",
|
||||
vi: "vn",
|
||||
export const languageNames: Record<string, string> = {
|
||||
en: "English",
|
||||
de: "Deutsch",
|
||||
ca: "Català",
|
||||
es: "Español",
|
||||
fr: "Français",
|
||||
"pt-BR": "Português (BR)",
|
||||
cs: "Čeština",
|
||||
fi: "Suomi",
|
||||
tr: "Türkçe",
|
||||
ru: "Русский",
|
||||
uk: "Українська",
|
||||
ar: "العربية",
|
||||
th: "ไทย",
|
||||
vi: "Tiếng Việt",
|
||||
ja: "日本語",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
};
|
||||
|
||||
export const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { language = "en" } = useSelector((state: RootState) => state.ui);
|
||||
const dispatch = useDispatch();
|
||||
const handleChange = (event: any) => {
|
||||
@@ -35,64 +43,15 @@ export const LanguageSelector = () => {
|
||||
value={language}
|
||||
onChange={handleChange}
|
||||
size="small"
|
||||
// sx={{
|
||||
// minWidth: 80,
|
||||
// "& .MuiSelect-select": {
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// justifyContent: "center",
|
||||
// },
|
||||
// "& .MuiSelect-icon": {
|
||||
// alignSelf: "center",
|
||||
// },
|
||||
// }}
|
||||
>
|
||||
{languages.map((lang) => {
|
||||
let parsedLang = lang === "en" ? "gb" : lang;
|
||||
|
||||
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"
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{flag && <span className={flag} />}
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ textTransform: "uppercase" }}
|
||||
>
|
||||
{lang}
|
||||
</Box>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
{languages.map((lang) => (
|
||||
<MenuItem
|
||||
key={lang}
|
||||
value={lang}
|
||||
>
|
||||
{languageNames[lang] ?? lang}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,52 +1,13 @@
|
||||
import Radio from "@mui/material/Radio";
|
||||
import type { RadioProps } from "@mui/material/Radio";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Circle, CircleDot } from "lucide-react";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
interface RadioInputProps extends RadioProps {}
|
||||
|
||||
export const RadioInput = ({ ...props }: RadioInputProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Radio
|
||||
{...props}
|
||||
icon={
|
||||
<Circle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
checkedIcon={
|
||||
<CircleDot
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
padding: 0,
|
||||
mt: theme.spacing(0.5),
|
||||
color: theme.palette.text.secondary,
|
||||
"&.Mui-checked": {
|
||||
color: theme.palette.primary.main,
|
||||
"& svg circle": {
|
||||
fill: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
fontSize: 16,
|
||||
},
|
||||
"& svg": {
|
||||
stroke: "currentColor",
|
||||
},
|
||||
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
|
||||
stroke: "currentColor",
|
||||
fill: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <Radio {...props} />;
|
||||
};
|
||||
|
||||
export const RadioWithDescription = ({
|
||||
@@ -79,7 +40,6 @@ export const RadioWithDescription = ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
"& .MuiButtonBase-root": {
|
||||
p: 0,
|
||||
mr: theme.spacing(6),
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -11,15 +11,14 @@ interface SelectInputProps<T> extends Omit<SelectProps<T>, "label"> {
|
||||
fieldLabel?: string;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
placeholderColor?: string;
|
||||
}
|
||||
|
||||
const SelectInputInner = <T,>(
|
||||
{ fieldLabel, required, placeholder, placeholderColor, ...props }: SelectInputProps<T>,
|
||||
{ fieldLabel, required, placeholder, ...props }: SelectInputProps<T>,
|
||||
ref: React.ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled;
|
||||
const emptyPlaceholderColor = theme.palette.text.secondary;
|
||||
|
||||
const renderValue = (selected: unknown) => {
|
||||
const isMultiple = Boolean((props as { multiple?: boolean }).multiple);
|
||||
|
||||
@@ -21,24 +21,16 @@ export const SwitchComponent = forwardRef<HTMLInputElement, SwitchComponentProps
|
||||
},
|
||||
}}
|
||||
sx={[
|
||||
{
|
||||
"& .MuiSwitch-switchBase": {
|
||||
"&.Mui-checked": {
|
||||
color: "#E0E0E0",
|
||||
"& + .MuiSwitch-track": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
opacity: 1,
|
||||
border: 0,
|
||||
...(dualOption
|
||||
? [
|
||||
{
|
||||
"& .MuiSwitch-track": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...(dualOption && {
|
||||
"& .MuiSwitch-track": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
opacity: 1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...additionalSx,
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { FieldLabel } from "./FieldLabel";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface TextInputProps extends Omit<TextFieldProps, "label"> {
|
||||
fieldLabel?: string;
|
||||
@@ -12,39 +13,53 @@ interface TextInputProps extends Omit<TextFieldProps, "label"> {
|
||||
}
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
|
||||
{ fieldLabel, required, ...props },
|
||||
{ fieldLabel, required, sx, ...props },
|
||||
ref
|
||||
) {
|
||||
const theme = useTheme();
|
||||
|
||||
const innerSx = {
|
||||
width: "100%",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: props.multiline ? "auto" : 34,
|
||||
fontSize: typographyLevels.base,
|
||||
overflow: "hidden",
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
"& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active":
|
||||
{
|
||||
WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.default} inset !important`,
|
||||
WebkitTextFillColor: `${theme.palette.text.primary} !important`,
|
||||
caretColor: theme.palette.text.primary,
|
||||
transition: "background-color 5000s ease-in-out 0s",
|
||||
},
|
||||
};
|
||||
|
||||
const input = (
|
||||
<TextField
|
||||
{...props}
|
||||
inputRef={ref}
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 34,
|
||||
fontSize: typographyLevels.base,
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}}
|
||||
sx={fieldLabel ? innerSx : { ...innerSx, ...sx }}
|
||||
/>
|
||||
);
|
||||
|
||||
if (fieldLabel) {
|
||||
return (
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
<Stack
|
||||
spacing={theme.spacing(LAYOUT.XXS)}
|
||||
sx={sx}
|
||||
>
|
||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||
{input}
|
||||
</Stack>
|
||||
|
||||
@@ -3,8 +3,17 @@ import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
||||
import type { ToggleButtonProps } from "@mui/material/ToggleButton";
|
||||
import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
|
||||
export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
const selectedBg = isDark
|
||||
? alpha(theme.palette.primary.main, 0.22)
|
||||
: alpha(theme.palette.primary.main, 0.06);
|
||||
const selectedHoverBg = isDark
|
||||
? alpha(theme.palette.primary.main, 0.3)
|
||||
: alpha(theme.palette.primary.main, 0.1);
|
||||
return (
|
||||
<ToggleButton
|
||||
{...props}
|
||||
@@ -17,8 +26,18 @@ export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
boxShadow: "none",
|
||||
color: theme.palette.text.secondary,
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: selectedBg,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 500,
|
||||
"&:hover": {
|
||||
backgroundColor: selectedHoverBg,
|
||||
},
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
@@ -34,7 +53,6 @@ export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps)
|
||||
sx={{
|
||||
"& .MuiToggleButtonGroup-grouped": {
|
||||
borderColor: theme.palette.divider,
|
||||
|
||||
borderRadius: 0,
|
||||
"&:first-of-type": {
|
||||
borderTopLeftRadius: 2,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Slide from "@mui/material/Slide";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useSidebar } from "@/Hooks/useSidebar";
|
||||
|
||||
interface BulkActionsBarProps {
|
||||
selectedCount: number;
|
||||
onCancel: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BulkActionsBar = ({
|
||||
selectedCount,
|
||||
onCancel,
|
||||
children,
|
||||
}: BulkActionsBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isOpen = selectedCount > 0;
|
||||
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const { width, collapsedWidth, transition } = useSidebar();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
bottom: theme.spacing(LAYOUT.XS),
|
||||
left: isSmall ? collapsedWidth : width,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
pointerEvents: "none",
|
||||
zIndex: theme.zIndex.snackbar,
|
||||
transition: transition.replace("width", "left"),
|
||||
}}
|
||||
>
|
||||
<Slide
|
||||
direction="up"
|
||||
in={isOpen}
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
>
|
||||
<Paper
|
||||
elevation={6}
|
||||
sx={{
|
||||
pointerEvents: "auto",
|
||||
px: LAYOUT.MD,
|
||||
py: LAYOUT.XS,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: LAYOUT.XS,
|
||||
boxShadow: theme.shadows[8],
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={LAYOUT.XXS}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={600}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{t("pages.common.monitors.actions.bulkSelected", {
|
||||
count: selectedCount,
|
||||
})}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onCancel}
|
||||
aria-label={t("common.buttons.cancel")}
|
||||
>
|
||||
<X size={18} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={LAYOUT.XXS}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Slide>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Select, Button } from "@/Components/inputs";
|
||||
import { Select, Autocomplete, Button, TextField } from "@/Components/inputs";
|
||||
import { ColoredLabel } from "@/Components/design-elements";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
|
||||
import type { MonitorType } from "@/Types/Monitor";
|
||||
import type { Tag } from "@/Types/Tag";
|
||||
import { Typography, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -28,22 +30,34 @@ export const ControlsFilter = ({
|
||||
setSelectedStatus,
|
||||
selectedState,
|
||||
setSelectedState,
|
||||
tagOptions,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
onClearFilters,
|
||||
}: {
|
||||
showTypes?: boolean;
|
||||
selectedTypes?: MonitorType[];
|
||||
setSelectedTypes?: React.Dispatch<React.SetStateAction<MonitorType[]>>;
|
||||
selectedStatus: string;
|
||||
setSelectedStatus: React.Dispatch<React.SetStateAction<string>>;
|
||||
selectedState: string;
|
||||
setSelectedState: React.Dispatch<React.SetStateAction<string>>;
|
||||
selectedStatus?: string;
|
||||
setSelectedStatus?: React.Dispatch<React.SetStateAction<string>>;
|
||||
selectedState?: string;
|
||||
setSelectedState?: React.Dispatch<React.SetStateAction<string>>;
|
||||
tagOptions?: Tag[];
|
||||
selectedTags?: string[];
|
||||
setSelectedTags?: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
onClearFilters: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isFilterActive =
|
||||
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
|
||||
(selectedTypes?.length ?? 0) > 0 ||
|
||||
Boolean(selectedStatus) ||
|
||||
Boolean(selectedState) ||
|
||||
(selectedTags?.length ?? 0) > 0;
|
||||
const selectedTagObjects = (tagOptions ?? []).filter((tag) =>
|
||||
(selectedTags ?? []).includes(tag.id)
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
@@ -66,34 +80,68 @@ export const ControlsFilter = ({
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<Select
|
||||
placeholder="Status"
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
>
|
||||
{statuses.map((status) => (
|
||||
<MenuItem
|
||||
key={status}
|
||||
value={status}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{status}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="State"
|
||||
value={selectedState}
|
||||
onChange={(e) => setSelectedState(e.target.value)}
|
||||
>
|
||||
{states.map((state) => (
|
||||
<MenuItem
|
||||
key={state}
|
||||
value={state}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{state}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{setSelectedStatus && (
|
||||
<Select
|
||||
placeholder="Status"
|
||||
value={selectedStatus ?? ""}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
>
|
||||
{statuses.map((status) => (
|
||||
<MenuItem
|
||||
key={status}
|
||||
value={status}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{status}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
{setSelectedState && (
|
||||
<Select
|
||||
placeholder="State"
|
||||
value={selectedState ?? ""}
|
||||
onChange={(e) => setSelectedState(e.target.value)}
|
||||
>
|
||||
{states.map((state) => (
|
||||
<MenuItem
|
||||
key={state}
|
||||
value={state}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{state}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
{tagOptions && setSelectedTags && (
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={tagOptions}
|
||||
value={selectedTagObjects}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(_: unknown, newValue: Tag[]) => {
|
||||
setSelectedTags(newValue.map((tag) => tag.id));
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
renderOptionContent={(option) => (
|
||||
<ColoredLabel
|
||||
text={option.name}
|
||||
color={option.color}
|
||||
/>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder={t("common.table.headers.tags")}
|
||||
/>
|
||||
)}
|
||||
slotProps={{
|
||||
popper: {
|
||||
sx: { width: "fit-content !important", minWidth: 280 },
|
||||
},
|
||||
}}
|
||||
sx={{ minWidth: 180 }}
|
||||
/>
|
||||
)}
|
||||
{isFilterActive && (
|
||||
<Button
|
||||
variant="contained"
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Box, Typography, Stack, useTheme, GlobalStyles } from "@mui/material";
|
||||
import Map, { Marker, Popup } from "react-map-gl/maplibre";
|
||||
import type { MapRef } from "react-map-gl/maplibre";
|
||||
import type { FlatGeoCheck } from "@/Types/GeoCheck";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatStatusCode } from "@/Utils/statusCode";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
interface GeoChecksMapProps {
|
||||
@@ -14,6 +16,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
|
||||
const [selectedCheck, setSelectedCheck] = useState<FlatGeoCheck | null>(null);
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mapPopupStyles = (
|
||||
<GlobalStyles
|
||||
@@ -174,7 +177,7 @@ export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
|
||||
>
|
||||
Status Code:
|
||||
</Typography>{" "}
|
||||
{selectedCheck.statusCode}
|
||||
{formatStatusCode(selectedCheck.statusCode, t)}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<Typography
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
ResponsiveContainer,
|
||||
Text,
|
||||
} from "recharts";
|
||||
import type { TooltipProps } from "recharts";
|
||||
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
@@ -18,13 +20,14 @@ import {
|
||||
tooltipDateFormatLookup,
|
||||
} from "@/Utils/TimeUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
import type { GroupedCheck } from "@/Types/Check";
|
||||
import type { RootState } from "@/Types/state";
|
||||
|
||||
type XTickProps = {
|
||||
x: number;
|
||||
y: number;
|
||||
payload: { value: any };
|
||||
payload: { value: string };
|
||||
range: string;
|
||||
};
|
||||
|
||||
@@ -46,12 +49,9 @@ export const XTick = ({ x, y, payload, range }: XTickProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type ResponseTimeToolTipProps = {
|
||||
active?: boolean | undefined;
|
||||
payload?: any[];
|
||||
label?: string | number;
|
||||
type ResponseTimeToolTipProps = TooltipProps<ValueType, NameType> & {
|
||||
range: string;
|
||||
theme: any;
|
||||
theme: Theme;
|
||||
uiTimezone: string;
|
||||
};
|
||||
|
||||
@@ -68,7 +68,8 @@ const ResponseTimeToolTip = ({
|
||||
if (!active) return null;
|
||||
|
||||
const format = tooltipDateFormatLookup(range);
|
||||
const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0);
|
||||
console.log(payload);
|
||||
const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0);
|
||||
return (
|
||||
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
|
||||
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) =>
|
||||
>
|
||||
<RadialBar
|
||||
dataKey="value"
|
||||
background={{ fill: theme.palette[palette].light }}
|
||||
background={{ fill: theme.palette.action.disabledBackground }}
|
||||
>
|
||||
<Cell visibility={"hidden"} />
|
||||
<Cell fill={theme.palette[palette].main} />
|
||||
@@ -75,13 +75,14 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) =>
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
variant="eyebrow"
|
||||
color={theme.palette[palette].main}
|
||||
textAlign={"center"}
|
||||
>
|
||||
{msg[palette]}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
variant="h1"
|
||||
textAlign={"center"}
|
||||
>{`${avg?.toFixed()}ms`}</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -14,3 +14,4 @@ export * from "./charts/HistogramPageSpeedDetails";
|
||||
export * from "./charts/HistogramPageSpeedDetailsTooltip";
|
||||
export * from "./charts/HistogramInfrastructure";
|
||||
export * from "./HeaderMonitorsSummary";
|
||||
export * from "./BulkActionsBar";
|
||||
@@ -68,7 +68,12 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
||||
gap: theme.spacing(LAYOUT.MD),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(LAYOUT.XS),
|
||||
"& svg": { stroke: theme.palette.text.secondary },
|
||||
"& svg": {
|
||||
height: 16,
|
||||
width: 16,
|
||||
opacity: 0.81,
|
||||
stroke: theme.palette.text.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { toggleSidebar } from "@/Features/UI/uiSlice.js";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSidebar from "@/Hooks/useSidebar";
|
||||
import KingIcon from "@/assets/icons/checkmate-icon.svg?react";
|
||||
|
||||
export const Logo = (props: StackProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -23,19 +24,21 @@ export const Logo = (props: StackProps) => {
|
||||
sx={{ cursor: "pointer" }}
|
||||
{...props}
|
||||
>
|
||||
<Typography
|
||||
minWidth={theme.spacing(16)}
|
||||
minHeight={theme.spacing(16)}
|
||||
display={"flex"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
bgcolor={theme.palette.primary.main}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontSize={18}
|
||||
<Box
|
||||
sx={{
|
||||
width: theme.spacing(16),
|
||||
height: theme.spacing(16),
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
"& svg": {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
C
|
||||
</Typography>
|
||||
<KingIcon />
|
||||
</Box>
|
||||
<Box
|
||||
overflow={"hidden"}
|
||||
sx={{
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
AlertTriangle,
|
||||
Wifi,
|
||||
Wrench,
|
||||
Database,
|
||||
ScrollText,
|
||||
Settings,
|
||||
HelpCircle,
|
||||
MessageCircle,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
User,
|
||||
Lock,
|
||||
Users,
|
||||
Tag,
|
||||
} from "lucide-react";
|
||||
|
||||
export const getMenu = (t: Function) => {
|
||||
@@ -41,6 +42,11 @@ export const getMenu = (t: Function) => {
|
||||
path: "notifications",
|
||||
icon: <Icon icon={Bell} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.tags"),
|
||||
path: "tags",
|
||||
icon: <Icon icon={Tag} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.checks"),
|
||||
path: "checks",
|
||||
@@ -64,7 +70,7 @@ export const getMenu = (t: Function) => {
|
||||
{
|
||||
name: t("components.sidebar.menu.logs"),
|
||||
path: "logs",
|
||||
icon: <Icon icon={Database} />,
|
||||
icon: <Icon icon={ScrollText} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.settings"),
|
||||
|
||||
@@ -22,7 +22,9 @@ export const NavItem = ({
|
||||
}) => {
|
||||
const { collapsed } = useSidebar();
|
||||
const theme = useTheme();
|
||||
const iconStroke = selected ? theme.palette.primary.main : theme.palette.text.secondary;
|
||||
const iconStroke = selected
|
||||
? theme.palette.sidebar.accent
|
||||
: theme.palette.text.secondary;
|
||||
|
||||
const buttonBgColor = selected ? theme.palette.action.selected : "transparent";
|
||||
const buttonBgHoverColor = selected
|
||||
@@ -79,7 +81,7 @@ export const NavItem = ({
|
||||
},
|
||||
".MuiListItemButton-root:hover &": {
|
||||
"& svg path, & svg line, & svg polyline, & svg rect, & svg circle": {
|
||||
stroke: theme.palette.primary.main,
|
||||
stroke: theme.palette.sidebar.accent,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Divider from "@mui/material/Divider";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useSidebar } from "@/Hooks/useSidebar.js";
|
||||
import { Logo } from "@/Components/sidebar/Logo";
|
||||
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
|
||||
import { getMenu, getAccountMenu } from "@/Components/sidebar/Menu";
|
||||
import { NavItem } from "@/Components/sidebar/NavItem";
|
||||
import { StarPrompt } from "@/Components/sidebar/StarPrompt";
|
||||
import { AuthFooter } from "@/Components/sidebar/Authfooter";
|
||||
@@ -20,7 +20,7 @@ import { setCollapsed } from "@/Features/UI/uiSlice";
|
||||
const URL_MAP: Record<string, string> = {
|
||||
support: "https://discord.com/invite/NAb6H3UTjK",
|
||||
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
|
||||
docs: "https://bluewavelabs.gitbook.io/checkmate",
|
||||
docs: "https://checkmate.so/docs",
|
||||
changelog: "https://github.com/bluewave-labs/checkmate/releases",
|
||||
};
|
||||
|
||||
@@ -33,7 +33,6 @@ export const Sidebar = () => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const menu = getMenu(t);
|
||||
const bottomMenu = getBottomMenu(t);
|
||||
const accountMenu = getAccountMenu(t);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -103,23 +102,6 @@ export const Sidebar = () => {
|
||||
})}
|
||||
</List>
|
||||
<StarPrompt />
|
||||
<List
|
||||
component="nav"
|
||||
disablePadding
|
||||
sx={{ px: theme.spacing(LAYOUT.SM) }}
|
||||
>
|
||||
{bottomMenu.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
selected={selected}
|
||||
onClick={() => handleNavClick(item.path)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider sx={{ borderColor: theme.palette.divider }} />
|
||||
|
||||
<AuthFooter
|
||||
|
||||
@@ -89,7 +89,9 @@ const uiSlice = createSlice({
|
||||
},
|
||||
|
||||
setTimezone: (state, action: PayloadAction<{ timezone: string }>) => {
|
||||
state.timezone = action.payload.timezone;
|
||||
if (action.payload.timezone) {
|
||||
state.timezone = action.payload.timezone;
|
||||
}
|
||||
},
|
||||
setLanguage: (state, action: PayloadAction<string>) => {
|
||||
state.language = action.payload;
|
||||
|
||||
@@ -57,7 +57,11 @@ export const usePost = <B = any, R = any>() => {
|
||||
...config?.headers,
|
||||
},
|
||||
});
|
||||
toastSuccess(res.data?.msg || "Operation successful");
|
||||
if (res.data?.success === false) {
|
||||
toastError(res.data?.msg || "Operation failed");
|
||||
} else {
|
||||
toastSuccess(res.data?.msg || "Operation successful");
|
||||
}
|
||||
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -4,8 +4,9 @@ export const useToast = () => {
|
||||
const showToast = (message: string, options?: ToastOptions) => {
|
||||
toast.dismiss();
|
||||
const baseStyle: React.CSSProperties = {
|
||||
whiteSpace: "pre-line",
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
};
|
||||
toast(message, {
|
||||
...options,
|
||||
@@ -19,6 +20,8 @@ export const useToast = () => {
|
||||
showToast(msg, { ...opts, type: "error" });
|
||||
const toastInfo = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "info" });
|
||||
const toastWarning = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "warning" });
|
||||
|
||||
return { showToast, toastSuccess, toastError, toastInfo };
|
||||
return { showToast, toastSuccess, toastError, toastInfo, toastWarning };
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { post } from "@/Utils/ApiClient";
|
||||
import { useToast } from "@/Hooks/UseToast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "@/Utils/logger";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean;
|
||||
msg: string;
|
||||
data: {
|
||||
monitors: Monitor[];
|
||||
failedCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UseBulkMonitorActionsReturn {
|
||||
selectedRows: string[];
|
||||
setSelectedRows: (rows: string[]) => void;
|
||||
handleBulkPause: () => Promise<void>;
|
||||
handleBulkResume: () => Promise<void>;
|
||||
handleCancelSelection: () => void;
|
||||
}
|
||||
|
||||
export const useBulkMonitorActions = (
|
||||
refetch: () => void,
|
||||
page?: number
|
||||
): UseBulkMonitorActionsReturn => {
|
||||
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||
const { toastSuccess, toastError, toastInfo, toastWarning } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Clear selection when page changes
|
||||
useEffect(() => {
|
||||
setSelectedRows([]);
|
||||
}, [page]);
|
||||
|
||||
const executeBulkAction = async (pause: boolean) => {
|
||||
try {
|
||||
const res = await post<ApiResponse>("/monitors/bulk/pause", {
|
||||
monitorIds: selectedRows,
|
||||
pause,
|
||||
});
|
||||
|
||||
const affectedCount = res.data?.data?.monitors?.length ?? 0;
|
||||
const failedCount = res.data?.data?.failedCount ?? 0;
|
||||
|
||||
if (affectedCount === 0) {
|
||||
const key = pause
|
||||
? "pages.common.monitors.bulkPause.alreadyPaused"
|
||||
: "pages.common.monitors.bulkPause.alreadyRunning";
|
||||
toastInfo(t(key, { count: selectedRows.length }));
|
||||
} else if (failedCount > 0) {
|
||||
toastWarning(
|
||||
t("pages.common.monitors.bulkPause.partialFailure", {
|
||||
successCount: affectedCount,
|
||||
failedCount,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const key = pause
|
||||
? "pages.common.monitors.bulkPause.paused"
|
||||
: "pages.common.monitors.bulkPause.resumed";
|
||||
toastSuccess(t(key, { count: affectedCount }));
|
||||
}
|
||||
|
||||
setSelectedRows([]);
|
||||
refetch();
|
||||
} catch (err: unknown) {
|
||||
let errMsg = "An error occurred";
|
||||
|
||||
if (axios.isAxiosError(err)) {
|
||||
errMsg = err.response?.data?.msg || err.message || errMsg;
|
||||
} else if (err instanceof Error) {
|
||||
errMsg = err.message;
|
||||
}
|
||||
|
||||
logger.error("Bulk pause/resume failed", err instanceof Error ? err : undefined, {
|
||||
pause,
|
||||
});
|
||||
toastError(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkPause = async () => {
|
||||
await executeBulkAction(true);
|
||||
};
|
||||
|
||||
const handleBulkResume = async () => {
|
||||
await executeBulkAction(false);
|
||||
};
|
||||
|
||||
const handleCancelSelection = () => {
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
return {
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
handleBulkPause,
|
||||
handleBulkResume,
|
||||
handleCancelSelection,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export const useClientPagination = <T>(rows: T[], initialRowsPerPage = 10) => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage);
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
newPage: number
|
||||
) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleRowsPerPageChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
setPage(0);
|
||||
setRowsPerPage(Number(e.target.value));
|
||||
};
|
||||
|
||||
const pagedRows = rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
|
||||
|
||||
return {
|
||||
page,
|
||||
rowsPerPage,
|
||||
pagedRows,
|
||||
paginationProps: {
|
||||
component: "div" as const,
|
||||
count: rows.length,
|
||||
page,
|
||||
rowsPerPage,
|
||||
onPageChange: handlePageChange,
|
||||
onRowsPerPageChange: handleRowsPerPageChange,
|
||||
itemsOnPage: pagedRows.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
maintenanceWindowSchema,
|
||||
maintenanceWindowEditSchema,
|
||||
repeatOptions,
|
||||
type MaintenanceWindowFormData,
|
||||
} from "@/Validation/maintenanceWindow";
|
||||
@@ -32,7 +33,7 @@ export const useMaintenanceWindowForm = ({
|
||||
startTime: startDate.format("HH:mm"),
|
||||
duration: data.duration ?? 0,
|
||||
durationUnit: data.durationUnit ?? "minutes",
|
||||
monitors: [data.monitorId],
|
||||
monitors: data.monitorIds,
|
||||
};
|
||||
} else {
|
||||
const now = dayjs();
|
||||
@@ -47,6 +48,9 @@ export const useMaintenanceWindowForm = ({
|
||||
};
|
||||
}
|
||||
|
||||
return { schema: maintenanceWindowSchema, defaults };
|
||||
return {
|
||||
schema: data ? maintenanceWindowEditSchema : maintenanceWindowSchema,
|
||||
defaults,
|
||||
};
|
||||
}, [data]);
|
||||
};
|
||||
+130
-100
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
|
||||
import { DefaultHttpMethod, DefaultPageSpeedStrategy } from "@/Types/Monitor";
|
||||
import type { Monitor, MonitorType } from "@/Types/Monitor";
|
||||
|
||||
interface UseMonitorFormOptions {
|
||||
@@ -12,118 +13,147 @@ const getBaseDefaults = (data?: Monitor | null) => ({
|
||||
description: data?.description || "",
|
||||
interval: data?.interval || 60000,
|
||||
notifications: data?.notifications || [],
|
||||
tags: data?.tags || [],
|
||||
statusWindowSize: data?.statusWindowSize || 5,
|
||||
statusWindowThreshold: data?.statusWindowThreshold || 60,
|
||||
});
|
||||
|
||||
// Geo-check defaults — only http and ping support geo checks.
|
||||
const getGeoCheckDefaults = (data?: Monitor | null) => ({
|
||||
geoCheckEnabled: data?.geoCheckEnabled ?? false,
|
||||
geoCheckLocations: data?.geoCheckLocations || [],
|
||||
geoCheckInterval: data?.geoCheckInterval || 300000,
|
||||
});
|
||||
|
||||
export const getMonitorDefaults = (
|
||||
type: MonitorType,
|
||||
data: Monitor | null = null
|
||||
): MonitorFormData => {
|
||||
const base = getBaseDefaults(data);
|
||||
|
||||
let defaults: MonitorFormData;
|
||||
|
||||
switch (type) {
|
||||
case "http":
|
||||
defaults = {
|
||||
...base,
|
||||
...getGeoCheckDefaults(data),
|
||||
type: "http",
|
||||
url: data?.url || "",
|
||||
method: data?.method ?? DefaultHttpMethod,
|
||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||
useAdvancedMatching: data?.useAdvancedMatching || false,
|
||||
matchMethod: data?.matchMethod || "",
|
||||
expectedValue: data?.expectedValue || "",
|
||||
jsonPath: data?.jsonPath || "",
|
||||
customUpCodes: data?.customUpCodes || [],
|
||||
};
|
||||
break;
|
||||
case "ping":
|
||||
defaults = {
|
||||
...base,
|
||||
...getGeoCheckDefaults(data),
|
||||
type: "ping",
|
||||
url: data?.url || "",
|
||||
};
|
||||
break;
|
||||
case "port":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "port",
|
||||
url: data?.url || "",
|
||||
port: data?.port || 80,
|
||||
};
|
||||
break;
|
||||
case "docker":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "docker",
|
||||
url: data?.url || "",
|
||||
};
|
||||
break;
|
||||
case "game":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "game",
|
||||
url: data?.url || "",
|
||||
port: data?.port || 27015,
|
||||
gameId: data?.gameId || "",
|
||||
};
|
||||
break;
|
||||
case "grpc":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "grpc",
|
||||
url: data?.url || "",
|
||||
port: data?.port || 50051,
|
||||
grpcServiceName: data?.grpcServiceName || "",
|
||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||
};
|
||||
break;
|
||||
case "pagespeed":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "pagespeed",
|
||||
url: data?.url || "",
|
||||
strategy: data?.strategy ?? DefaultPageSpeedStrategy,
|
||||
};
|
||||
break;
|
||||
case "hardware":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "hardware",
|
||||
url: data?.url || "",
|
||||
secret: data?.secret || "",
|
||||
cpuAlertThreshold: data?.cpuAlertThreshold ?? 100,
|
||||
memoryAlertThreshold: data?.memoryAlertThreshold ?? 100,
|
||||
diskAlertThreshold: data?.diskAlertThreshold ?? 100,
|
||||
tempAlertThreshold: data?.tempAlertThreshold ?? 100,
|
||||
selectedDisks: data?.selectedDisks || [],
|
||||
};
|
||||
break;
|
||||
case "websocket":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "websocket",
|
||||
url: data?.url || "",
|
||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||
};
|
||||
break;
|
||||
case "dns":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "dns",
|
||||
url: data?.url || "",
|
||||
dnsServer: data?.dnsServer || "",
|
||||
dnsRecordType: data?.dnsRecordType || "A",
|
||||
};
|
||||
break;
|
||||
default:
|
||||
defaults = {
|
||||
...base,
|
||||
...getGeoCheckDefaults(data),
|
||||
type: "http",
|
||||
url: "",
|
||||
method: DefaultHttpMethod,
|
||||
ignoreTlsErrors: false,
|
||||
useAdvancedMatching: false,
|
||||
matchMethod: "",
|
||||
expectedValue: "",
|
||||
jsonPath: "",
|
||||
customUpCodes: [],
|
||||
};
|
||||
}
|
||||
|
||||
return defaults;
|
||||
};
|
||||
|
||||
export const useMonitorForm = ({
|
||||
data = null,
|
||||
defaultType = "http",
|
||||
}: UseMonitorFormOptions = {}) => {
|
||||
return useMemo(() => {
|
||||
const type = data?.type || defaultType;
|
||||
const base = getBaseDefaults(data);
|
||||
|
||||
let defaults: MonitorFormData;
|
||||
|
||||
switch (type) {
|
||||
case "http":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "http",
|
||||
url: data?.url || "",
|
||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||
useAdvancedMatching: data?.useAdvancedMatching || false,
|
||||
matchMethod: data?.matchMethod || "",
|
||||
expectedValue: data?.expectedValue || "",
|
||||
jsonPath: data?.jsonPath || "",
|
||||
};
|
||||
break;
|
||||
case "ping":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "ping",
|
||||
url: data?.url || "",
|
||||
};
|
||||
break;
|
||||
case "port":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "port",
|
||||
url: data?.url || "",
|
||||
port: data?.port || 80,
|
||||
};
|
||||
break;
|
||||
case "docker":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "docker",
|
||||
url: data?.url || "",
|
||||
};
|
||||
break;
|
||||
case "game":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "game",
|
||||
url: data?.url || "",
|
||||
port: data?.port || 27015,
|
||||
gameId: data?.gameId || "",
|
||||
};
|
||||
break;
|
||||
case "grpc":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "grpc",
|
||||
url: data?.url || "",
|
||||
port: data?.port || 50051,
|
||||
grpcServiceName: data?.grpcServiceName || "",
|
||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||
};
|
||||
break;
|
||||
case "pagespeed":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "pagespeed",
|
||||
url: data?.url || "",
|
||||
};
|
||||
break;
|
||||
case "hardware":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "hardware",
|
||||
url: data?.url || "",
|
||||
secret: data?.secret || "",
|
||||
cpuAlertThreshold: data?.cpuAlertThreshold ?? 100,
|
||||
memoryAlertThreshold: data?.memoryAlertThreshold ?? 100,
|
||||
diskAlertThreshold: data?.diskAlertThreshold ?? 100,
|
||||
tempAlertThreshold: data?.tempAlertThreshold ?? 100,
|
||||
selectedDisks: data?.selectedDisks || [],
|
||||
};
|
||||
break;
|
||||
case "websocket":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "websocket",
|
||||
url: data?.url || "",
|
||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
defaults = {
|
||||
...base,
|
||||
type: "http",
|
||||
url: "",
|
||||
ignoreTlsErrors: false,
|
||||
useAdvancedMatching: false,
|
||||
matchMethod: "",
|
||||
expectedValue: "",
|
||||
jsonPath: "",
|
||||
};
|
||||
}
|
||||
|
||||
return { schema: monitorSchema, defaults };
|
||||
return { schema: monitorSchema, defaults: getMonitorDefaults(type, data) };
|
||||
}, [data, defaultType]);
|
||||
};
|
||||
@@ -78,6 +78,14 @@ function buildDefaults(data: Notification | null): NotificationFormData {
|
||||
accessToken: data.accessToken || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "ntfy") {
|
||||
return {
|
||||
type: "ntfy",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
topic: data.topic || "",
|
||||
};
|
||||
}
|
||||
// Default: email (covers both data === null and data.type === "email")
|
||||
return {
|
||||
type: "email",
|
||||
|
||||
@@ -18,6 +18,7 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
|
||||
systemEmailHost: data?.systemEmailHost || "",
|
||||
systemEmailUser: data?.systemEmailUser || "",
|
||||
systemEmailAddress: data?.systemEmailAddress || "",
|
||||
systemEmailDisplayName: data?.systemEmailDisplayName || "",
|
||||
systemEmailConnectionHost: data?.systemEmailConnectionHost || "localhost",
|
||||
systemEmailTLSServername: data?.systemEmailTLSServername || "",
|
||||
systemEmailPort: data?.systemEmailPort,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { statusPageSchema, type StatusPageFormData } from "@/Validation/statusPage";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
import {
|
||||
DEFAULT_STATUS_PAGE_THEME,
|
||||
DEFAULT_STATUS_PAGE_THEME_MODE,
|
||||
} from "@/Types/StatusPage";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import { resolveTimezone } from "@/Utils/TimeUtils";
|
||||
|
||||
interface UseStatusPageFormOptions {
|
||||
data?: StatusPage | null;
|
||||
@@ -26,9 +31,10 @@ export const useStatusPageForm = ({
|
||||
const defaults: StatusPageFormData = {
|
||||
companyName: data?.companyName || "",
|
||||
url: data?.url || generateDefaultUrl(),
|
||||
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
customDomain: data?.customDomain ?? null,
|
||||
timezone: resolveTimezone(data?.timezone),
|
||||
type: data?.type || ["uptime"],
|
||||
color: data?.color || "#4169E1",
|
||||
color: data?.color || "#13715B",
|
||||
monitors: data?.monitors || [],
|
||||
isPublished: data?.isPublished ?? false,
|
||||
showCharts: data?.showCharts ?? true,
|
||||
@@ -36,6 +42,8 @@ export const useStatusPageForm = ({
|
||||
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
||||
showInfrastructure: data?.showInfrastructure ?? false,
|
||||
customCSS: data?.customCSS || "",
|
||||
theme: data?.theme ?? DEFAULT_STATUS_PAGE_THEME,
|
||||
themeMode: data?.themeMode ?? DEFAULT_STATUS_PAGE_THEME_MODE,
|
||||
logo: transformLogo(data?.logo),
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useMemo } from "react";
|
||||
import { tagSchema, type TagFormData } from "@/Validation/tag";
|
||||
import type { Tag } from "@/Types/Tag";
|
||||
|
||||
interface UseTagsFormOptions {
|
||||
data?: Tag | null;
|
||||
}
|
||||
|
||||
export const useTagsForm = ({ data }: UseTagsFormOptions) => {
|
||||
return useMemo(() => {
|
||||
const defaults: TagFormData = {
|
||||
name: data?.name ?? "",
|
||||
color: data?.color ?? "#13715B",
|
||||
};
|
||||
|
||||
return { schema: tagSchema, defaults };
|
||||
}, [data]);
|
||||
};
|
||||
@@ -1,15 +1,18 @@
|
||||
import { Stack } from "@mui/material";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HeaderTeamControls } from "./components/HeaderTeamControls";
|
||||
import { TeamTable } from "./components/TeamTable";
|
||||
import { InviteTeamMemberDialog } from "./components/InviteTeamMemberDialog";
|
||||
import { AddTeamMemberDialog } from "./components/AddTeamMemberDialog";
|
||||
import { EmptyState } from "@/Components/design-elements";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { User, UserRole } from "@/Types/User";
|
||||
|
||||
export const TabTeam = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [filter, setFilter] = useState<UserRole | "">("");
|
||||
const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
|
||||
const [addMemberDialogOpen, setAddMemberDialogOpen] = useState(false);
|
||||
@@ -33,6 +36,9 @@ export const TabTeam = () => {
|
||||
refetch();
|
||||
};
|
||||
|
||||
const totalUsers = users?.length ?? 0;
|
||||
const noUsers = users !== undefined && totalUsers === 0;
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<HeaderTeamControls
|
||||
@@ -41,7 +47,15 @@ export const TabTeam = () => {
|
||||
onInviteClick={handleOpenInviteDialog}
|
||||
onAddMemberClick={handleOpenAddMemberDialog}
|
||||
/>
|
||||
<TeamTable users={filteredUsers} />
|
||||
{noUsers ? (
|
||||
<EmptyState
|
||||
fullscreen
|
||||
title={t("pages.account.team.fallback.title")}
|
||||
description={t("pages.account.team.fallback.description")}
|
||||
/>
|
||||
) : (
|
||||
<TeamTable users={filteredUsers} />
|
||||
)}
|
||||
<InviteTeamMemberDialog
|
||||
open={inviteDialogOpen}
|
||||
onClose={handleCloseInviteDialog}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useAddTeamMemberForm } from "@/Hooks/useAddTeamMemberForm";
|
||||
import type { AddTeamMemberFormData } from "@/Validation/addTeamMember";
|
||||
import type { UserRole, User } from "@/Types/User";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface AddTeamMemberDialogProps {
|
||||
open: boolean;
|
||||
@@ -82,38 +83,40 @@ export const AddTeamMemberDialog = ({
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(4)}
|
||||
>
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("common.form.name.option.firstName.label")}
|
||||
placeholder={t("common.form.name.option.firstName.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="lastName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("common.form.name.option.lastName.label")}
|
||||
placeholder={t("common.form.name.option.lastName.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("common.form.name.option.firstName.label")}
|
||||
placeholder={t("common.form.name.option.firstName.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="lastName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("common.form.name.option.lastName.label")}
|
||||
placeholder={t("common.form.name.option.lastName.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
@@ -151,36 +154,41 @@ export const AddTeamMemberDialog = ({
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.password.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
type="password"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="confirm"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.confirmPassword.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
type="password"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.password.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
type="password"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="confirm"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.confirmPassword.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
type="password"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { UserRole } from "@/Types/User";
|
||||
import { useInviteForm } from "@/Hooks/useInviteForm";
|
||||
import type { InviteFormData } from "@/Validation/invite";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
|
||||
|
||||
@@ -95,10 +96,7 @@ export const InviteTeamMemberDialog = ({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(4)}
|
||||
>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
|
||||
@@ -2,8 +2,10 @@ import Typography from "@mui/material/Typography";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Table } from "@/Components/design-elements";
|
||||
import { Pagination } from "@/Components/design-elements/Table";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import { useClientPagination } from "@/Hooks/useClientPagination";
|
||||
import type { User } from "@/Types/User";
|
||||
|
||||
interface TeamTableProps {
|
||||
@@ -14,6 +16,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { pagedRows, paginationProps } = useClientPagination(users);
|
||||
|
||||
const headers: Header<User>[] = [
|
||||
{
|
||||
@@ -51,10 +54,13 @@ export const TeamTable = ({ users }: TeamTableProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
headers={headers}
|
||||
data={users}
|
||||
onRowClick={isAdmin ? handleRowClick : undefined}
|
||||
/>
|
||||
<>
|
||||
<Table
|
||||
headers={headers}
|
||||
data={pagedRows}
|
||||
onRowClick={isAdmin ? handleRowClick : undefined}
|
||||
/>
|
||||
{users.length > 0 && <Pagination {...paginationProps} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -25,7 +25,7 @@ const Account = ({ open = "profile" }: AccountProps) => {
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<BasePage headerKey="account">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, newValue: number) => setActiveTab(newValue)}
|
||||
|
||||
@@ -95,7 +95,23 @@ const RegisterPage = () => {
|
||||
title={t("pages.auth.register.title")}
|
||||
subtitle={t("pages.auth.register.subtitle")}
|
||||
>
|
||||
{!token && <Alert severity="info">{t("pages.auth.register.setupNotice")}</Alert>}
|
||||
{!token && (
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={false}
|
||||
sx={(theme) => ({
|
||||
fontSize: 13,
|
||||
lineHeight: 1.55,
|
||||
color: theme.palette.text.secondary,
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 1,
|
||||
"& .MuiAlert-message": { padding: 0 },
|
||||
})}
|
||||
>
|
||||
{t("pages.auth.register.setupNotice")}
|
||||
</Alert>
|
||||
)}
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Table, Pagination, ValueLabel, StatusLabel } from "@/Components/design-elements";
|
||||
import {
|
||||
Table,
|
||||
Pagination,
|
||||
StatusLabel,
|
||||
StatusCodeLabel,
|
||||
} from "@/Components/design-elements";
|
||||
import Box from "@mui/material/Box";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import type { Check } from "@/Types/Check";
|
||||
@@ -29,7 +33,7 @@ export const ChecksTable = ({
|
||||
const { t } = useTranslation();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
|
||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
const getHeaders = () => {
|
||||
const headers: Header<Check>[] = [
|
||||
{
|
||||
id: "monitorName",
|
||||
@@ -63,13 +67,10 @@ export const ChecksTable = ({
|
||||
id: "statusCode",
|
||||
content: t("pages.checks.table.headers.statusCode"),
|
||||
render: (row) => {
|
||||
const code = row.statusCode;
|
||||
if (!code) return "N/A";
|
||||
const value = code < 300 ? "positive" : code < 400 ? "neutral" : "negative";
|
||||
return (
|
||||
<ValueLabel
|
||||
value={value}
|
||||
text={String(code)}
|
||||
<StatusCodeLabel
|
||||
statusCode={row.statusCode}
|
||||
message={row.message}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -85,7 +86,7 @@ export const ChecksTable = ({
|
||||
return headers;
|
||||
};
|
||||
|
||||
const headers = getHeaders(t, uiTimezone);
|
||||
const headers = getHeaders();
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {
|
||||
BasePage,
|
||||
EmptyState,
|
||||
TotalChecksBox,
|
||||
UpChecksBox,
|
||||
DownChecksBox,
|
||||
@@ -102,8 +103,19 @@ const Checks = () => {
|
||||
const downChecks = summaryResponse?.downChecks || 0;
|
||||
const upChecks = totalChecks - (summaryResponse?.downChecks || 0);
|
||||
|
||||
const noMonitors = !isLoadingMonitors && (monitorsResponse?.length ?? 0) === 0;
|
||||
if (noMonitors) {
|
||||
return (
|
||||
<EmptyState
|
||||
fullscreen
|
||||
title={t("pages.checks.fallback.title")}
|
||||
description={t("pages.checks.fallback.description")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<BasePage headerKey="checks">
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={4}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,7 @@ import Stack from "@mui/material/Stack";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BaseBox, ValueLabel } from "@/Components/design-elements";
|
||||
import { BaseBox, ValueLabel, StatusCodeLabel } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -20,6 +20,24 @@ interface CardDetailsProps {
|
||||
sx?: object;
|
||||
}
|
||||
|
||||
const SectionHeading = ({ children }: { children: React.ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const Cell = ({ children }: { children: React.ReactNode }) => (
|
||||
<Typography variant="body1">{children}</Typography>
|
||||
);
|
||||
|
||||
export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@@ -28,27 +46,24 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
if (!incident) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
sx={sx}
|
||||
>
|
||||
<Typography textTransform={"uppercase"}>
|
||||
{t("pages.incidents.dialog.details.title")}
|
||||
</Typography>
|
||||
<SectionHeading>{t("pages.incidents.dialog.details.title")}</SectionHeading>
|
||||
<BaseBox padding={LAYOUT.MD}>
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
<Typography textTransform={"uppercase"}>
|
||||
{t("pages.incidents.dialog.details.overview")}
|
||||
</Typography>
|
||||
<Divider />
|
||||
|
||||
<SectionHeading>{t("pages.incidents.dialog.details.overview")}</SectionHeading>
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.status")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<ValueLabel
|
||||
value={incident.status ? "negative" : "positive"}
|
||||
@@ -61,15 +76,17 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
</Grid>
|
||||
{monitor && (
|
||||
<>
|
||||
<Grid size={2}>{t("pages.incidents.dialog.details.monitor")}</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.monitor")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Typography>{monitor.name ?? "N/A"}</Typography>
|
||||
<Cell>{monitor.name ?? "N/A"}</Cell>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Typography>{t("pages.incidents.dialog.details.url")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.url")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Typography>{monitor.url ?? "N/A"}</Typography>
|
||||
<Cell>{monitor.url ?? "N/A"}</Cell>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
@@ -78,51 +95,53 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
</BaseBox>
|
||||
<BaseBox padding={LAYOUT.MD}>
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
<Typography textTransform={"uppercase"}>
|
||||
{t("pages.incidents.dialog.details.analysis")}
|
||||
</Typography>
|
||||
<Divider />
|
||||
<SectionHeading>{t("pages.incidents.dialog.details.analysis")}</SectionHeading>
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
<Grid size={6}>
|
||||
<Typography>{t("pages.incidents.dialog.details.timeline")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.timeline")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Typography>{t("pages.incidents.dialog.details.detailsLabel")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.detailsLabel")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Divider></Divider>
|
||||
<Divider />
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Divider></Divider>
|
||||
<Divider />
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Typography>{t("pages.incidents.dialog.details.startedAt")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.startedAt")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Typography>
|
||||
<Cell>
|
||||
{formatDateWithTz(incident.startTime, "D MMM YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Typography>{t("pages.incidents.dialog.details.statusCode")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.statusCode")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Typography>{incident.statusCode ?? "N/A"}</Typography>
|
||||
<Cell>
|
||||
<StatusCodeLabel
|
||||
statusCode={incident.statusCode}
|
||||
message={incident.message}
|
||||
/>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Typography>{t("pages.incidents.dialog.details.downtime")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.downtime")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Typography>{getIncidentsDuration(incident)}</Typography>
|
||||
<Cell>{getIncidentsDuration(incident)}</Cell>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Typography>{t("pages.incidents.dialog.details.message")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.message")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Typography>{incident.message ?? "N/A"}</Typography>
|
||||
<Cell>{incident.message ?? "N/A"}</Cell>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
@@ -130,60 +149,53 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
{!incident.status && (
|
||||
<BaseBox padding={LAYOUT.MD}>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Typography textTransform={"uppercase"}>
|
||||
<SectionHeading>
|
||||
{t("pages.incidents.dialog.details.resolutionDetails")}
|
||||
</Typography>
|
||||
<Divider />
|
||||
</SectionHeading>
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid size={2}>
|
||||
<Typography>{t("pages.incidents.dialog.details.resolvedAt")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.resolvedAt")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Typography>
|
||||
<Cell>
|
||||
{incident.endTime
|
||||
? formatDateWithTz(incident.endTime, "D MMM YYYY, h:mm A", uiTimezone)
|
||||
: "N/A"}
|
||||
</Typography>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Typography>
|
||||
{t("pages.incidents.dialog.details.resolutionType")}
|
||||
</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.resolutionType")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Typography>
|
||||
<Cell>
|
||||
{incident.resolutionType
|
||||
? t(
|
||||
`pages.incidents.dialog.details.resolutionTypes.${incident.resolutionType}`
|
||||
)
|
||||
: "N/A"}
|
||||
</Typography>
|
||||
</Cell>
|
||||
</Grid>
|
||||
{incident.resolvedBy && (
|
||||
<>
|
||||
<Grid size={2}>
|
||||
<Typography>
|
||||
{t("pages.incidents.dialog.details.resolvedBy")}
|
||||
</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.resolvedBy")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Typography>
|
||||
{incident.resolvedByEmail ?? incident.resolvedBy}
|
||||
</Typography>
|
||||
<Cell>{incident.resolvedByEmail ?? incident.resolvedBy}</Cell>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{incident.comment && (
|
||||
<>
|
||||
<Grid size={2}>
|
||||
<Typography>{t("pages.incidents.dialog.details.comment")}</Typography>
|
||||
<Cell>{t("pages.incidents.dialog.details.comment")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Typography>{incident.comment}</Typography>
|
||||
<Cell>{incident.comment}</Cell>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { BaseBox, ValueLabel } from "@/Components/design-elements";
|
||||
import { BaseBox, Icon, ValueLabel } from "@/Components/design-elements";
|
||||
import { CircleCheck, TriangleAlert, Bell, Wrench, Globe } from "lucide-react";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
@@ -25,6 +24,7 @@ const SummaryItem = ({ icon, label, value }: SummaryItemProps) => {
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(2)}
|
||||
minHeight={32}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -32,10 +32,10 @@ const SummaryItem = ({ icon, label, value }: SummaryItemProps) => {
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
{icon}
|
||||
<Typography variant="body2">{label}</Typography>
|
||||
<Typography variant="body1">{label}</Typography>
|
||||
</Stack>
|
||||
<Typography
|
||||
variant="body2"
|
||||
variant="body1"
|
||||
fontWeight={600}
|
||||
>
|
||||
{value}
|
||||
@@ -67,15 +67,11 @@ export const SummaryCard = ({
|
||||
>
|
||||
<Typography
|
||||
component="h2"
|
||||
sx={{
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
}}
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Divider />
|
||||
{children}
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
@@ -97,10 +93,13 @@ export const SummaryCardActiveIncidents = ({
|
||||
const activeCount = summary.totalActive;
|
||||
const hasActive = activeCount > 0;
|
||||
const color = hasActive ? theme.palette.error.main : theme.palette.success.main;
|
||||
const icon = hasActive ? (
|
||||
<TriangleAlert color={color} />
|
||||
) : (
|
||||
<CircleCheck color={color} />
|
||||
const icon = (
|
||||
<Box sx={{ color, display: "inline-flex" }}>
|
||||
<Icon
|
||||
icon={hasActive ? TriangleAlert : CircleCheck}
|
||||
size={32}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
const msg = t("pages.incidents.summaryCard.activeIncidents.active", {
|
||||
count: activeCount,
|
||||
@@ -133,10 +132,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
container
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
sx={{
|
||||
width: "100%",
|
||||
py: theme.spacing(0.5),
|
||||
}}
|
||||
sx={{ width: "100%", minHeight: 32 }}
|
||||
>
|
||||
<Grid
|
||||
size={{ xs: 12, lg: 5 }}
|
||||
@@ -147,7 +143,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
gap: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Globe />
|
||||
<Icon icon={Globe} />
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={500}
|
||||
@@ -159,9 +155,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
|
||||
<Grid
|
||||
size={{ xs: 12, md: 6, lg: 3 }}
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
sx={{ display: "flex" }}
|
||||
>
|
||||
<ValueLabel
|
||||
value={incident.status ? "negative" : "positive"}
|
||||
@@ -173,7 +167,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
size={{ xs: 12, md: 6, lg: 4 }}
|
||||
sx={{
|
||||
textAlign: { xs: "left", md: "right" },
|
||||
fontWeight: 500,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{duration}</Typography>
|
||||
@@ -190,22 +184,17 @@ export const SummaryCardLatestIncidents = ({
|
||||
summary,
|
||||
}: SummaryCardLatestIncidentsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const latestIncidents = summary?.latestIncidents ?? [];
|
||||
|
||||
return (
|
||||
<SummaryCard title={t("pages.incidents.summaryCard.latestIncidents.title")}>
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
{latestIncidents.slice(0, 3).map((incident, index) => (
|
||||
<Box key={incident.id}>
|
||||
<SummaryIncidentItem incident={incident} />
|
||||
{index < latestIncidents.length - 1 && (
|
||||
<Divider sx={{ mt: theme.spacing(2) }} />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
{latestIncidents.slice(0, 3).map((incident) => (
|
||||
<SummaryIncidentItem
|
||||
key={incident.id}
|
||||
incident={incident}
|
||||
/>
|
||||
))}
|
||||
</SummaryCard>
|
||||
);
|
||||
};
|
||||
@@ -224,17 +213,17 @@ export const SummaryCardStats = ({ summary }: SummaryCardStatsProps) => {
|
||||
return (
|
||||
<SummaryCard title={t("pages.incidents.summaryCard.incidentStats.title")}>
|
||||
<SummaryItem
|
||||
icon={<Bell size={18} />}
|
||||
icon={<Icon icon={Bell} />}
|
||||
label={t("pages.incidents.summaryCard.incidentStats.totalIncidents")}
|
||||
value={summary?.total || 0}
|
||||
/>
|
||||
<SummaryItem
|
||||
icon={<TriangleAlert size={18} />}
|
||||
icon={<Icon icon={TriangleAlert} />}
|
||||
label={t("pages.incidents.summaryCard.incidentStats.mostAffectedMonitor")}
|
||||
value={mostAffected}
|
||||
/>
|
||||
<SummaryItem
|
||||
icon={<Wrench size={18} />}
|
||||
icon={<Icon icon={Wrench} />}
|
||||
label={t("pages.incidents.summaryCard.incidentStats.avgResolutionTime")}
|
||||
value={
|
||||
summary.total > 0
|
||||
|
||||
@@ -75,7 +75,7 @@ export const ControlsIncidentFilter = ({
|
||||
variant="contained"
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
{t("pages.incidents.filters.clearFilters")}
|
||||
{t("common.buttons.clearFilters")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Dialog, TextField } from "@/Components/inputs";
|
||||
import { usePut } from "@/Hooks/UseApi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
interface DialogResolutionProps {
|
||||
open: boolean;
|
||||
@@ -19,7 +17,6 @@ export const DialogResolution = ({
|
||||
onResolved,
|
||||
}: DialogResolutionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [comment, setComment] = useState("");
|
||||
const { put: resolveIncident, loading: isResolving } = usePut();
|
||||
|
||||
@@ -50,22 +47,19 @@ export const DialogResolution = ({
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
confirmColor="error"
|
||||
cancelColor="primary"
|
||||
loading={isResolving}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<Box sx={{ mt: theme.spacing(4) }}>
|
||||
<TextField
|
||||
fieldLabel={t("pages.incidents.dialog.resolveIncident.option.comment.label")}
|
||||
placeholder={t(
|
||||
"pages.incidents.dialog.resolveIncident.option.comment.placeholder"
|
||||
)}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
fieldLabel={t("pages.incidents.dialog.resolveIncident.option.comment.label")}
|
||||
placeholder={t(
|
||||
"pages.incidents.dialog.resolveIncident.option.comment.placeholder"
|
||||
)}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Loaded 100 of 576 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user