From 88590301c0c6c48daf498cb067f9c18c9ac6df2a Mon Sep 17 00:00:00 2001 From: "Dr. Gorkem Cetin" Date: Fri, 11 Oct 2024 02:36:23 +0000 Subject: [PATCH 01/76] GITBOOK-21: No subject --- guides/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/guides/README.md b/guides/README.md index 3de3c7e59..d034f615b 100644 --- a/guides/README.md +++ b/guides/README.md @@ -20,7 +20,7 @@ layout: # Welcome to Uptime Manager -BlueWave Uptime Manager is an open-source server monitoring application used to track the operational status and performance of servers and websites. +BlueWave Uptime Manager is an open-source server monitoring application used to track the operational status and performance of servers and websites. It 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. @@ -41,7 +41,7 @@ If you have a question, head over to GitHub's [discussions](https://github.com/b * [x] Incidents at a glance * [x] Page speed monitoring * [x] E-mail notifications -* [ ] Scheduled maintenance (in the works) +* [x] Scheduled maintenance (in the works) ## **Roadmap (short term)** @@ -60,4 +60,3 @@ If you have a question, head over to GitHub's [discussions](https://github.com/b * [MUI (React framework)](https://mui.com/) * [Node.js](https://nodejs.org/en) * [MongoDB](https://mongodb.com) - From ba0a19d5fe8f1993444e13755df6161fe5572ff5 Mon Sep 17 00:00:00 2001 From: "Dr. Gorkem Cetin" Date: Fri, 11 Oct 2024 02:44:26 +0000 Subject: [PATCH 02/76] GITBOOK-22: No subject --- .../Screenshot 2024-10-10 at 10.37.36 PM.png | Bin 0 -> 308418 bytes guides/SUMMARY.md | 1 + guides/users-guide/maintenance-mode.md | 35 ++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 guides/.gitbook/assets/Screenshot 2024-10-10 at 10.37.36 PM.png create mode 100644 guides/users-guide/maintenance-mode.md diff --git a/guides/.gitbook/assets/Screenshot 2024-10-10 at 10.37.36 PM.png b/guides/.gitbook/assets/Screenshot 2024-10-10 at 10.37.36 PM.png new file mode 100644 index 0000000000000000000000000000000000000000..31dbacbe07362918f53f51a5d0ce6a1a25dbc211 GIT binary patch literal 308418 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 z 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

h(;ASiuM22!-7i$$Gi-VPxtNnQ&)I!UMqldpdB zu}>eA_ry)L+wTMd28yW3)$cJ@PWs}q`n3vfu1|t*7n$F=*4HgAvawNZC6D5RQ!_(F zvdlv*OI3l579yuh`_H6<#s=25o-pK*T6!~2gPUzK(nfUyr_buC9rJv+RjgZKCe$1= zx9{W^V*#8ou8EEp>v<#!W#d_DQRc83DOV)uGMqt+d+Rp!fb8FViZeL&Oz95MVB zS455K8eS({RP&ktbe!!^IN;IRE$TO#3$y04pLe|@?iEBJsG6%rEH#iXn)op%D%Q8H zRv*?$)I9>YJ_;A^AXls~F(aF5tuk~hzN2S|k{FvUE|^;FH}9Hr!LLE7eeU0WJw zc9Fmx`!q*hKXSGu+D#hR7;lS~WY1~A>%!dG^-QRWxvI))4kKlu7de^U zrwhlJ4CjhZkpI|Gc|ogX%zDg0kG#0*>8spBcKu5e(7k#4DK{pwF%eM%aTyro_qxr0 z%SeGjarP7|ZvCz*T;F+S%WX(oHEOt~DQtDj`NoN^QOngJDvEe|xIMeBNG_N5GLR|e z!ej)0+BQ1&ph=p?mP}C3-TD?hB<8G&@^NC}?pB(jw!V-Comrd1_#;#l+Qi8_uk)Ow zKO#6&cPg?#*@dybh_b}M@u=cbXWabkmXNvk$SEG~z#EyKdhC@n5byemx}^I4--PKn zG77wb#p{E%!r%73TyL<`uM~XKMR3AWo&6$5O$;nq3wb?%anZ$*|C%jgt#$UKoK*vQxh8@>Lmp5o=ROuO&S?HN zYIrbJ+~+Z9h)gv+VXCD4ZTiN@VFeb{@rJl4G2lEhvhA&hQKC5TIyurxk~4a@b^WHP z`JgVc_6V-n2G{yjz~GR!!P;zFol{n>wL7tM^c-t7A~P+^$#IC=LKq{{yn8x%cz0mG#U9(OcEf~0UKg;sq%bhU5qEABg7hNROp?(gR=@pt4^92Zp@pEls*P&hFu_RA}4tg!O8Oq=itlA!WdDs6Olu z1=PsURJ6l3a+g?^ts|E1YiN`w9=tL25~@wHQ0lqIKe*@;eAUqxD}mo+z|0fIwwmI|lP zFv0gM-_B04DyXSaAi2|gPjn~u-XRBR1Ke}=Sk@F1+i0x+*_?ILxm#3r>f0-4yt1L@ zgMp0ft1mB=m-D7b1`rnX=nM+MKThcxlqAx#vNI&TAD!bIs$L(zNEw&T83$U`<9)xA)}*bS!ZnwkaHU;`^PkW|ci8 z>r_{msO4vhWzSd*V&JrKvcFa2O8gKs2lgT5oN;XTaXIIc&9iqsAC>vVq<}u8g3va- zoBOjav$k(&DS+5R4VdssC=h5Cs^+lEgUh|I7KR@k$hoBjDeY`|lKH$|*3-Ys9J8Y& zt$C#n)cp92EX3ugr+8P7Oy6h*i#n16-LhI(kukt^`86rU5ZOjTQ(KjblGzpSue}kp z>}r$BS0-FhHd9sRaUegbcAfV*l&`F<*l`@ayGU3|Nd*xRuc&HTr+5cSCeKt7O;u^Y zJC#-EekswTa(fRsTR{N_b*SmuBQ$j$EsT@(?CKm?4=kXzQ6cIC;~Ffdj2D9NZX)$+I!~kw9IefMggy`0ShwUB#)p`Ity*%Ts$eED7rPMWyG3u30~e*8dNX4Ud6a)0P3EKN4wYca{nmMt{+c^LRBRL zM3M|0z;DY7!6^Xa#<7>~V*gxa(L1;VARgI_Z^c6i;LZBr!3S~$cFjwlZkGBruUD

6=!a2_m{Pre}i_$Dl?Z`JM8t5w|S% zeKwK#MdGpgo0s9Um$tV@<N$t zw7l<(h#SC5q1f-Gz^yRU%qr(rsMB*>|D;py5ceA3yL$7G07E&In^-kcpVJR; zYMcm;*uIfMl!fTrQZv^gb#<+~RgDAnL5Gv$KLLub*KkzpIlL`w5Y>#=$GHVe1@}4J zBtH9MQ%mn9shFuo;=~4%bCQgaWzf1~&bZI1gKKDCc}Y}Ibi?Yq%8cBQ;Zhse=e|^+ zVpexZHon^jC<*$TgYiRt(eVKoCMSw)7?QYs`Su6}08Zuhe%9hR)p{x@qVFx!aNae=SZ`(&5A^i$NY+}eOq+M< zxs>04P{n(Q&0M#`zjxCrfWv^dQS4bLQ4xw*QMZAuV=VYfKFYvYufsoLQY9&Q%TCD- zh#0@9((FRC-U`g^wv+ElHqd)1*e|#DN0nRLID3c;ltG;NL#{T=nKX<$I1R>~qZM?x zklk)q1d*St-zAaERl`X*88L)NUR7|H!S=EuLbVRQ+NffMWc5v@#K@6nFSh6VOVZd^ z3S)NM8!81i$;7OPMcMQ{9c*Y?P9YTkmPf8F_L1j>vPw@cpjs*;EX&ck%Y@s2(LlSY z09cMFW-(BeRxW)nx6ETJX{R^4_@Z3~R(o)cV2R1PI$Qvlg8|a*G(egsPqTbL)yf94 zYBvq#KBHmx_JhFXko?K-h#khd*A1^n8sA&%AH7b1|AsZ^bo?Lj{~P@L`=NXFwCFb; zPBKFu9(oAFj~Z8_)!EBHem}fkPI7sArP49054NWMP)VV*BRVy+d7&5I2-yl~ARo6g zQ`IHB!qx5&k}wlezGJmVQZG%C{ta#d59%4xO%A{V>J8-grB@-Th^>h6DDV?7lUA7) zr?%L%?%<*MG+9ga3S%V`WooA=dF0gTubrn->LPj?!D(Q6du5T0WyAHz_Fb4mzI9_E zK*j!26u*t%Qo5jm9V8$ik~Ad7eaVltQnB7)rBOxwoHP3FA$qxq@MyOO#fCLk70g0x zB7AgQV$G($L6G$)opHandu*nNyQ^i%^5}y%h|Ty2lBE9q`Fz8B8N!Jc%3qAXiPzme z5koFB+RUIgyL)eMv=c|2a$!gfd`sQc6j;E(i}l#dI*6mUT8ubA2~YjgcjQQ9>oo{8 zb#cpxur^&Izm}>Jy-;|KdxHeYFpGNDjcgUm1jH%L2XmI~oQ)2w1d@^;Tiuav@=C=3 zr>SM1;j;EE1a~7Wh6Pl&Jrdk9YsrV@R7~Mxq4LDBaYpb!Z&Ver*Yo`oULdzNqjq;J z_AjiB69UWzT%aI2#b>G#z$@rkuRUrew|ND8zV#{xGq8B_ir;Rk)e%=te;Qq4!)ZRd z!q#csi#?3e&1db4JCLAg*v}^gY;r-7KtmS%0Ei7Pc1N>fb4 zK)r``kFX@Ob$!I}V8-$jUT+tgpP%_i%fjWsoJ2I;XW~uEM8P7jEjg^+Bp{z0p0p>lZ@AvklV08EZUDXO`%q?YIAj|l( z=1kFPh(AUHX)|hbW+|t=+_Ow#yFaYHSJ2;7DIHAKHy-2!Eijv%zjf_s^jN!}yJIrc zW4psjs6hO4z&HPUmiXt!l~#4{`v%vM(9u*E+KayF3<9#@Xmg0oTuT{$iFkG7#qAZ8 zq@CnqN;{o3J+h;FWDO0y>LK4;f4Yv5Ppc=55bdz~PYBTVQjk{B0dtXb>eOP($b0Jd$dYY=Vl9lepB9@N>5&1F zS21*xjwjJ@ha(sn!e8<^(+91V5>-H%&BOFkglQ=&=pqxCg z(e3(3LN}}!%UDI=fqlNz9S6*=$BAp_?j&w|Yp!`QMmlCa6{2jbWN>rA7ODU1=oGXUweZ8li!j=A_ofpU5fn1kLN|%*&Q?)WPoD2EY|ot0 zYT34q9Q`ny&g>xoF-^o|CNwnKk&`sOIbCN5clSikJ(>+r(k_vPEG(KsCNeiy{2tY% zCQB&M)8k!LMq#HX7Y&`v6QU>9GQPW@d?#*&a?#LS8@&#EHFk3R;Nz1y1V|2mfpBPl zR|&wZh}tE~E3*SO@rjqRw3m3HqdBZM@Ai#`1fE>zi<}-KuU*piQuK6q5T!yIFOO0K zH9DCSWMJJ}Eg4#`hn=29ZUTX5Zu2Rf|AR^LuSE5!M4XGDb(bgWi(j;AGpmIV*_{p)^_&BypseM( z?lIT33oFOva-MDy$B`;6?%q0q^Cpfur_wa_SC8P0-~l-)lR!gImL&@p=^oPbl?+(A zE25qFp(k-gO55MP3|HlB+>ItgGv#`X=^r;arGxPWpZ$K3v-g5G6g5}bjJu3VRh{HM2Qwg-N!44P@Wo@spB;Bhc5W=0AekAqS_GIEVx0r!of{}7-DmdEp3>gxi#SJ~ zo+LMz8v`LLS^r8g&~q^AzYN2iGrWVePO7~f>l}_vpv2?j)Hcyw`;W8ha zYhM)kr<*!m{1(W0G{aD%Q*-|r5hJ;O=?hmnkTp{&cnn-NhUSe`0gVXzXiv{C+iA-( znfh$#T1j?k$8xfor=xaV=M|tMk&TEI6&JS%Ls5K(nn^J9`upM*DPL5yXC&DT^!#?f z_HN@%_B1?3GiF)_X=>L@?!wDr#!pqXY_u1n{9%#fKn1tLpYay|JX62)-K*%Bt#32y zETI^Mn3*0ZkQST*QJ$#hPOvn!tf-c{yG zy6E*SZRI0=d03k(Wc?sH^uNRNiq<4IicUOl6h;LW6Fd``Pe zn4`Mh`6v~+!+B5YChgr#A{dibGD(@$w>~~WFuVMS7b=-seHXEw0O|$*i;zpUWn;&n znvCH}W_3#wBVKo~D5?T*u>(~Ahy=)UnFe&XSn}KzR(*I=e~e$fbbAz-J(*c-1#66` z^o%#2ym^_{nZHr`uzNrja$`vs0(tBbI+d&@aBSGBVC4!>2_wCV3IrZ`Z{ zc{LtdG_q*?tMgU8XNv%3P1@)sPAA{7sz86-d@^dmLN&YY>a*Gd4tBsRLQ^Zk|9Feb zhcDNX-fcu}YE)r;^ln^;meIMZcB$ewPrH~RLkAF$wBF*E+IV(m6;t&hF8Hn+KyD&k zBIBv>YL{8oZ7gN(ZiTJ8oI~9$Ddx4F8MXT}OEzU4dh3HPp(9O$v(hFWd0*eU@N;p= zb(x+&W6QV>@x(t}dQu(z{7p;B-9inommzG~cwYQR>BrOWrAvE;dk_U!KW$t0t2ZZK z*11@r^PN1O@ZPDS^v#aVTPWl!1HGJl3GbJLg+YZKE6?9wJCS4ksN9);ZwQna1zVgN z@O`~Fq)IIiRQRG;{C7sr|AoElAK_sFFtm}@UGFKr+zQUYOYMRR&U(%q&}N(f2Z{qs z@4BDg`S9W#_2pY*0Mr&ae|G7g$3I*v00iHsbOr;C1)y9ezYpFe*IdY6gcPYCHE?a? zV0%6#r1}9vv#KwR=<~U6KPK9-q7VFQXx2J4?(L{vhoh%X{cD*0$Dkf?2>i4Y$Xdpe_xk`WHu|AOuLuX?-f|}BJEnpi%K;c? zBBl?3ah3p-Mn8bD$n0zmFt#@wDR;e5zI1$X19*DeJ=zg>-iV=RhPoiCaqqPJ6)_H& z^IG)hoj*pA8U|(-i@!(CCm?@2_UDknzm5$(1-V*T9MBm~X>kG>kj0^>G z*yNwgX2<<L?Ji)=RW<>b?qB%CO3<3R=j*Ab|2%vGU~_>T6J<8R=YYT7 z%G_|4w;HdBm~zrN!^3wl7eGGV4eQN0`M>1mf9Bc%CPpCfAIS`}w5s@ylTP^`cqssq zW(d?UCbWT&86ctzm$Bw zVfo{0%Q>>!PdDlm+GGh5|>K4-ih0G-Dm&lGxW_lX$Bb~n{)b5nyaGYY|B(C-?Z#i@nr`;K%t6reZ}|UKn#AFZIRReg`!U}(x%7=P^e6iM zZ^!;rhNqzK#}aOS5(2KgTZvGtcw&EE73{*Cb^FNU5AD(EC;{cnuHnR-*~GC9?!y~H zTF(0~bLN>Bl3KMe;EJti^$z^J08ZPt@?)kg1YyMVf&la19jCP}{A0Yp%|qeLS2G%m zngfkwxE0Xiz-nJYyIbS;mwnhTb)uMuvb_%yq(Jx?L)pT4;1ti_sNUD|1z7yxc?D^3;~bnE^J6G+V<{T8-+D?V;}$?IR=s{7d3#xhgOUC>*z#Y9r92@H9=gqNc}NA2 zuRn#q6#Fz|BV+TTI9a5&*ssAo?O-YJ*TAH@J|O=(`Uo45yg@8Af9jvBY(>g|!5I5} zkF`uc!0A!lYf+YQ#^cy;67gJdF2YMRfamQFFw}GO=}*>;asgPj=*E}LL14geuBj3l zt*02TvWI45nuDey0cP+x{I7y4X81dH5?YfV)<3g=|CN8^(=B|(=38Xf|4RiRlAZDX zYeZJni?$lCi{LwVTi1U{78kXo?-93*Z?7vOj8r^haidTF5#v$=%*yo4oUH9X58NMX z|7qISw&1C1&EHTWAbFUoo}SU@miWX(MYE)*1aV{IEyU`(hWLHlmN)hWeAaO-aTV|% zdmgQ|o#Xh2N3VVgMM(Nid~))zj`?>z(m@-Rodk2}AdJp=-sDtaj2GYE*Vq*uy)xFU zMI48pP<39%~R{BzbXAc zru#2B`}50FIL~)fcG!xFmBqnyT3F2D>?7$YAS?{=zET9R{WXpy?Se<0&}V3jH*8Dp z0pg0d(ci);sl&xUMZ;YaTSXuN9Z15*ssDZEzw$b%emW1p@uyf-A}+t5C{$2relX&* z6$x6EOT4znauoi}BHb9 zSq;b~9jhbT%gprFc4rm>!E0+-OEG62pw;5ni516loC=O2=4z;S-oP zY2l6+!CU?^VjX3nt=Suet<%#lYuwjnIn;TB$hWvwLoVjsZT;5_&HF(?RlRb#pdmn~-s6quj_GPs+9k8U)- zP|>fwo_NM#FT-X76A_m<{96(m?-_4l0NI0T_ww3xragztuztp$TmP}p<^Km{D>vAb zb9`Lq5wGW}EA)rao7J}8*&MJC4U*OifN!NUp$lDFDspV?7m|S0+Z*n5$=jfrSNQL& z6k#Zr-j$0M*~(J^&d}m{T?E6pF8S2^1H7!esAIbS_MVl!TWn%Z;`Qv)d%3bVS?t8| z`I`G@q@3FJitUQ>iXwb*iVtlY$d%^aX=1>$|8XqeQK|1=D-B{}1r`PeO(FU2x4pT4 z^QP70>pkM?DgQDv%xGYg>)=#m<*N50h6YVZR%+z^QUOdpk?sIhvxrM+`@@yEd2Jv0RKAdLNsgIb$gNNr9hQ^*uMPOm=iL9el^`tjEiP;I%d0S>u8G}wu^-1xG8BcU zfVSDMi+L-+;U@QaSt)6;vjgfm8abh^5w9}EA*QZEaX`fW45bMdZ96Iy@sT&VXJb0j zU*C0iW~Jg-!sd$@FVNn8(2~&l4*PhFIWvxB0MCQO#(w{3Jsb`B@w;taf%R3PLvmG; zs`yUi!ca0fY3J|pkM4f_@0LxoxjQ#%9Cd8X@92y4Svb$P#E>NSW}B_6q*z zcb@GX%y`xd4~drVzXN_cml(@B!_I?yIiQ(XkN{U6dW_{rE3jcnF%>tc`w001*bB7t zCg;@$T+FR3zE0T)T;G?+g&~prr{VAtbPwer?S;+le3)q7*$@ABa!_d*n{;KN-mcgM z@ua@I-M9<~ESs;uo99{I3SmxMjyoSky5#NSHgu8=Dt2+g1jNe1U!}782D#G1TIhYw zf7i40=htMDoCY8v2FY<)qpcJJpNt(yg{d zj|RP(0}Tf+AD2C`e|r8vEFgs=#h>KL_qzU`m8VV(iHW$ri)fA7aWmFTD6H+|zgwFB z@J!DE=2PBJd4;Jkovzp+K7s`14EC{5ewO=(@Qj+R_Zo1`XD@OZSPmfzUk^Z)VIQ z0n_p5N&T0#z3(iw|E0N;&#dafIQnz{KVqWNP8r}Xy8GJw_XVR$8MN~H5`U;HA@+e| z@$-KPK%c4NbE_&qHsDXP(uEzXuKlJz+n|?<0Cd+o5W8a## z#r6yA*t_e2cco>sq-;>>?i;0+x}6PRd}sBdx{yaImt2K)Ub*K7d7637I@kkdCEa@8 zBdKq~)DK@ELm(H(jF6Al8FJM3Arx;EecmpqCz|#ie7Ga!$vckj_JbYy)`d9Q*`pH; zTL!fF-!wIW#Q8ZM(A$3L9}nR;{Fcd#+YNIpBbKIH>EMJ0QJ3JL0Ja!&GR^Au^QVM~ z#4p0?n&EQM$31RU&M|gE-I|;z1TtiVkgEG+JbUf;sFdc#5lTevo>x^f$gZi1>aigG zQ)9{vNS9=;dy~(;4pD}<7Dzib!oyuovNA07$zjWx+zP>EX@9x9{~(0_l2ZRr9-jg= z#J~I#_`7Za{%P+wzQ-9ZpY9mQ{1MboPybZTcNCXau0xa`=M%KPS$)#@zsm(euC-4^ zACQZY?@o3<$H}hT1DvczUO6p<7LwDNpd)o1*dDaGkt2_E)e%5AoQG}}Pb7}z{gCar z?%3Vd+P^pC{OfLi=5YN?^TzUWUBTa@Z5@LcrtZ#Gz{0yj+CpE#SX2uBPOo9 z;(8=zqWALpKIZuG;n46A@=ar!8lU~Jg;!%=m*R6M5I6mZic)sJSjy^qw&z?S-yRuj z(G_4Svlp3-go}I*t{!}FtS!|XYz`(rPS}hKn|^mj;)jdl_69s2;E4aO!_)L19{IbX z`#*w6SsPBFzlFwl@-$a!tqOnl$M2%Mk7J)2G);dzJ2O^lQ)%2aaV!4o>!CeW6;?Xr zFO;lBF>}kS+-)v7kulv2$+NNhdJgB*Lzl;=pk6ynAxp-cK#0(Pjd3`Ol)N{Z!l=Yp z0g63VOIz4n>!A@inw)Tu`-kpUC9m)PgX)KZf3$1sz-{-#mmy;R$pTzKB0prmVuVsC z%=z{f?0&A+htssP+ij807a)9T8r!JjNP!3LE9YpvT%GQTxpvfNmbzd6D0B&A1n=iL6zctcassT^k{u9=?E0;2&ym|QaxUT)U%x$h;j#|n%+%D(#1LBfO!tn= z;W=7?oIYD0sCp}2K^yj@^$6ea?$fH{0Vn)K@~*pfAL;40{K*BUdj$@akZ)*B-~rcn zx3y#U*qZ!~WdG9%?zJI$AFt&66>E=0YSuhKeKsZ8Twm~(d#_~W{;GGEk!ObAfxPSf z-C3Eh-7^2#T*gi9JXA19mk5l$z!SwQ==FHJI9*WhLdn!ViFF5XP_C)w@1CZK2*7Ml zEpHQOwBH5!1HK2{U;ZuVuCqGPb#I{0^PRJXlv?_JpqY8k3InV;(>hndpq)$jO46mD z?c=vdK0*J8=BVXINBe)_*0dG<1T3LGN7q4bGpuyYc0S^BuAnz^_`L$l2Oj1zPloI=Jfwg_@( zs0)u(2=8svmIv-!LRC~d?m%W_kqx<$@HrmGk=CPe=LBr|n}PSiEVD@$SrkR1w92R& zJIBUTTH|b1*p)70@R-n)3H!yLLHA#AIf-KuICg-G`};?%#ukj@`}M{_jP3iEpQ?`J z69Rf7-6y388|QQam63tnTF3V8dh@=(=W$fhWe^^&Z=PkKL7`zcjE_AgiRITVcUGEsdJm4e{9vb8lW=a@ezC%<%?nmr zhvz1NXcpe&f}BYC6d4MccKIu6_u2BSGQ^ZdT_~3DnYpB@4b$1lE!e0ru?o2UMGPvk zNe}ZBYF;c!3mYUXj(EiW${Y3OQCzs`&%?nu+<` z8`0;F%oOu;XXuV*Zu@e3$(~q_h%@5b>zn>5C(>7+XTAG!wo}*IkPDC1PV1AWJxIi= z+@H&p26w#eE$QV1;rA13y>YyP^TW~IITzJNB8}^Yj~D+fGC&ge&i^g)pG^JxAV*YOB+}6DGLw+jXhwt#A1@Z+EJhEmM_5 zy(LI*F8@ebfkf`{k3??%0MDZnV!;b76)4tDl>~m^{C^CZ@9KdD*t%|+{zNrHg-@Kw z&r3DDHdFkND~|gp35bP(*j|_`)biTNrRwNG&nLTQ90q6SfC#m&ffWe@ABjJ6b(o}% zjCj}4sDVCYhBbaAv5<5&Hg?Bh?;m3Gj_bj@ z=>M@2K+e9V#qlX}Sy^&5Z_Lt)kGvVk19Z+C6Yot!RzoibDMLCweDMZzA8Sso41*=B z3i~so*}4PsGDvkjp>@ygYNyIQ#zg!ucHw8aaWUX%;P-PaTWJ$&-|sJfHp39}oOmv*B}^ zlbTKye^mZRx8XBU4q33=23D@KL4Oc=9JuQb2GY~+o0mdQzIOn}NhGROGYoai;dqdz(V^ch$n>TlM##*^FW zhp#!uDr7Zr9pyd(jXCXE8*_#>d!A65BU7Kmsy;xgaEB8Pai!O%7uoVCOOM z@O2bgj~&h0Vr(!!P=&CNX!cXMADKp=Q16qlE$}VY3_XDa`tik>MGOCWhXkcD6=7g@ zCz6e&Fyotlr|Bd|ITR0h7Em16)MG5w982<=E{d|73c=&?&`qRKOcfgg%&CAq>3+xA z6h$H`rhr*@)XnA;TVmW4m(Sc%ugPBTWIrnuVj}RAKuRQ%{e;QP+{9k0f5C>_WUDeC zkRhw^3ChEO-OOrs$NM$-I)){~tXzTIf$%m`@CcxDgNp?{@|VXfg#lA6+KAs zaCA+A^7+U#bjI0x;76{vB@)Ef5^6=8~u_cAjhPxfNo(Atup;Eqs>k#HTIV)pLg8`!>8tH>g7rHpKcQO5awIypy9wC3Tb%J zO6}eegS((ByQ`mFP+E_SOW!4yV%#pymk8*_STsAAb4T zzsSV!dO89R5};qo_QYUW$VS$@Dx8U6jVxM4cESFjK-s12FYIOb25K9MgrMQC)fn+e)DJ0OR{6!|hxDOH?SwmvbC)s2c zK5%;}1@t8J9?O#{$W*Uz*j{D5g>QSZ!=5J)NO1(xJWFeM`OWaqGtt~NQxMPZ9Ul~= zAv|}&SqWfHq|rxP;tF zc!l!k-Y1r_@Vp^+7Id%mO)8dsD(AS0+f%o}EO@lt^eoE?HCqhB-{(m=cw+8hHzWIb zfTQh(p>A-8)B>?R)+;l5d0??gk112RVFj;=x1>`xB?lnlo9iIA!l@}5yMSHd&F;xu zrA6)pmlZ!A8HT1k*p-|H`LLah7~jZ@$)&Of*=U^rrZ;;Vwf~Wr=1J5#o2I=?0VY7{ z49uA&%HmBw680MEYdHzh!v09{ZEger*!-p82#gc-$4+bp9j^_AXAB34`YRf#9(v%AtabvqRzhXPk zUa&`te0*wGNo<+&$pEO|k`7bO`2!>2!qm53IGJ)w(BOXbg4LUI-b{6^?h%&fn298< zIQe>{F#L|vGal{mdou_Xw zsGFFk>wyEXNx1)mREs+!?^x`PS2Ay3LHZ!DvEQCggXHf0F3EndP1z(L?!DT@7Kv(p zWwiiNg*@`}LC}D8t63O)=E)lHlQjNy)099Vp->-o9`#^FNTksg$)8k@60hZFnJ=7c zKC_2_N<-`qxU`c=VX9ghJYdyJLzdBwJp@Rhpw^LZX#HeT-!*s(+kzFw_i`=vy;v2H-`c$7CwE{e4qHJc#TnUt8_b9Se;Av;M+=k{ikk`^*h>2-HRW7CePqcT11;Ztn-7H9-T zK-J0;A;{c=S7#&VDNye8_~1K%*6HkPWSU3KBh?Rvb=B~ zwzDB`vcHRck

+ +Here's how to set up a new maintenance window: + +**General settings:** + +* **Maintenance repeat**: Select how often the maintenance window should repeat (e.g., don't repeat, daily, weekly). +* **Date**: Select the date the maintenance window should start. +* **Start time**: Set the time of day the window begins. +* **Duration**: Specify how long the maintenance window should last (in seconds, minutes, or hours). + +**Monitor related settings:** + +* **Friendly Name**: Provide a descriptive name for the maintenance window (e.g., "Maintenance at 10:37 PM for 30 minutes"). +* **Add Monitors**: Select the specific monitors (URLs) for which this maintenance window will apply by typing and selecting from the list. + +Once all fields are filled, click **Create maintenance** to save the new window. + +### Editing a maintenance window + +* To edit an existing window, click the gear icon under **Actions** in the table view. +* Update the required fields and save your changes. + From 07e82edab76feafb4e1b1b7fcceea60015a0703d Mon Sep 17 00:00:00 2001 From: "Dr. Gorkem Cetin" Date: Fri, 11 Oct 2024 02:46:44 +0000 Subject: [PATCH 03/76] GITBOOK-23: No subject --- guides/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/guides/README.md b/guides/README.md index d034f615b..146de46e5 100644 --- a/guides/README.md +++ b/guides/README.md @@ -34,25 +34,25 @@ If you have a question, head over to GitHub's [discussions](https://github.com/b ## Features -* [x] Completely open source, deployable on your servers -* [x] Website monitoring -* [x] Port monitoring -* [x] Ping monitoring -* [x] Incidents at a glance -* [x] Page speed monitoring -* [x] E-mail notifications -* [x] Scheduled maintenance (in the works) +* Completely open source, deployable on your servers +* Website monitoring +* Port monitoring +* Ping monitoring +* Incidents at a glance +* Page speed monitoring +* E-mail notifications +* Scheduled maintenance ## **Roadmap (short term)** -* [ ] Memory, disk and CPU monitoring -* [ ] 3rd party integrations -* [ ] DNS monitoring -* [ ] SSL monitoring +* Memory, disk and CPU monitoring +* 3rd party integrations +* DNS monitoring +* SSL monitoring ## **Roadmap (long term)** -* [ ] Status pages +* Status pages ## Tech stack From 0476644f44ad181bed66f4b6506a8cb681acec4c Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:43:54 -0500 Subject: [PATCH 04/76] Update for name changes --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa2ccf3f9..d96c99d0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ -# Contributing to Uptime Manager application +# Contributing to Checkmate First off, thanks for taking the time to contribute! ❤️ -All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how the Uptime Manager project handles them. Please make sure to read the relevant section before making your contribution. It will make it much easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 +All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how the Checkmate project handles them. Please make sure to read the relevant section before making your contribution. It will make it much easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 > And if you like the project, but don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: > - Star the project @@ -77,7 +77,7 @@ Once it's filed: This section guides you through submitting an enhancement suggestion for the application, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community understand your enhancements and find related suggestions. - Make sure that you are using the latest version. -- Make sure you run the code locally. The Uptime Manager [documentation](https://bluewavelabs.gitbook.io/uptime-manager) covers it. +- Make sure you run the code locally. The Checkmate [documentation](https://bluewavelabs.gitbook.io/checkmate) 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. From 4ed995692d76a233eaf9a1b25862f7e7c887f743 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jan 2025 15:30:51 -0800 Subject: [PATCH 05/76] Refactor incident endpoints to use aggregation --- Server/controllers/checkController.js | 11 +- Server/db/models/Check.js | 7 ++ Server/db/mongo/modules/checkModule.js | 163 ++++++++++++------------- Server/routes/checkRoute.js | 4 +- Server/service/networkService.js | 1 + Server/service/statusService.js | 3 +- 6 files changed, 93 insertions(+), 96 deletions(-) diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js index 8988df6d8..ae6b96dbd 100644 --- a/Server/controllers/checkController.js +++ b/Server/controllers/checkController.js @@ -44,7 +44,7 @@ class CheckController { } }; - getChecks = async (req, res, next) => { + getChecksByMonitor = async (req, res, next) => { try { await getChecksParamValidation.validateAsync(req.params); await getChecksQueryValidation.validateAsync(req.query); @@ -54,19 +54,18 @@ class CheckController { } try { - const checks = await this.db.getChecks(req); - const checksCount = await this.db.getChecksCount(req); + const result = await this.db.getChecksByMonitor(req); return res.success({ msg: successMessages.CHECK_GET, - data: { checksCount, checks }, + data: result, }); } catch (error) { next(handleError(error, SERVICE_NAME, "getChecks")); } }; - getTeamChecks = async (req, res, next) => { + getChecksByTeam = async (req, res, next) => { try { await getTeamChecksParamValidation.validateAsync(req.params); await getTeamChecksQueryValidation.validateAsync(req.query); @@ -75,7 +74,7 @@ class CheckController { return; } try { - const checkData = await this.db.getTeamChecks(req); + const checkData = await this.db.getChecksByTeam(req); return res.success({ msg: successMessages.CHECK_GET, diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index 1946f2c59..dba47a12e 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -12,6 +12,13 @@ const BaseCheckSchema = mongoose.Schema({ immutable: true, index: true, }, + + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + index: true, + }, /** * Status of the check (true for up, false for down). * diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 0a552012d..870011d9f 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -2,6 +2,8 @@ import Check from "../../models/Check.js"; import Monitor from "../../models/Monitor.js"; import User from "../../models/User.js"; import logger from "../../../utils/logger.js"; +import { ObjectId } from "mongodb"; + const SERVICE_NAME = "checkModule"; const dateRangeLookup = { day: new Date(new Date().setDate(new Date().getDate() - 1)), @@ -56,41 +58,6 @@ const createCheck = async (checkData) => { } }; -const getChecksCount = async (req) => { - const monitorId = req.params.monitorId; - const dateRange = req.query.dateRange; - const filter = req.query.filter; - // Build query - const checksQuery = { monitorId: monitorId }; - // Filter checks by "day", "week", or "month" - if (dateRange !== undefined) { - checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] }; - } - - if (filter !== undefined) { - checksQuery.status = false; - switch (filter) { - case "all": - break; - case "down": - break; - case "resolve": - checksQuery.statusCode = 5000; - break; - default: - logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecksCount", - }); - break; - } - } - - const count = await Check.countDocuments(checksQuery); - return count; -}; - /** * Get all checks for a monitor * @async @@ -98,33 +65,32 @@ const getChecksCount = async (req) => { * @returns {Promise>} * @throws {Error} */ - -const getChecks = async (req) => { +const getChecksByMonitor = async (req) => { try { const { monitorId } = req.params; let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; - // Default limit to 0 if not provided - limit = limit === undefined ? 0 : limit; + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); - // Default sort order is newest -> oldest - sortOrder = sortOrder === "asc" ? 1 : -1; + // Match + const matchStage = { + monitorId: ObjectId.createFromHexString(monitorId), + status: false, + ...(dateRange && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; - // Build query - const checksQuery = { monitorId: monitorId }; - // Filter checks by "day", "week", or "month" - if (dateRange !== undefined) { - checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] }; - } - // Filter checks by status if (filter !== undefined) { - checksQuery.status = false; switch (filter) { case "all": break; case "down": break; case "resolve": - checksQuery.statusCode = 5000; + matchStage.statusCode = 5000; break; default: logger.warn({ @@ -136,16 +102,33 @@ const getChecks = async (req) => { } } - // Need to skip and limit here + //Sort + sortOrder = sortOrder === "asc" ? 1 : -1; + + // Pagination let skip = 0; if (page && rowsPerPage) { skip = page * rowsPerPage; } - const checks = await Check.find(checksQuery) - .skip(skip) - .limit(rowsPerPage) - .sort({ createdAt: sortOrder }); - return checks; + + const checks = await Check.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: sortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]); + + return checks[0]; } catch (error) { error.service = SERVICE_NAME; error.method = "getChecks"; @@ -153,32 +136,32 @@ const getChecks = async (req) => { } }; -const getTeamChecks = async (req) => { +const getChecksByTeam = async (req) => { try { - const { teamId } = req.params; let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); - // Get monitorIDs - const userMonitors = await Monitor.find({ teamId: teamId }).select("_id"); - - //Build check query - // Default limit to 0 if not provided - limit = limit === undefined ? 0 : limit; - // Default sort order is newest -> oldest - sortOrder = sortOrder === "asc" ? 1 : -1; - - const checksQuery = { monitorId: { $in: userMonitors } }; + const { teamId } = req.params; + const matchStage = { + teamId: ObjectId.createFromHexString(teamId), + status: false, + ...(dateRange && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + // Add filter to match stage if (filter !== undefined) { - checksQuery.status = false; switch (filter) { case "all": break; case "down": - checksQuery.status = false; break; case "resolve": - checksQuery.statusCode = 5000; + matchStage.statusCode = 5000; break; default: logger.warn({ @@ -190,27 +173,34 @@ const getTeamChecks = async (req) => { } } - if (dateRange !== undefined) { - checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] }; - } + sortOrder = sortOrder === "asc" ? 1 : -1; - // Skip and limit for pagination + // pagination let skip = 0; if (page && rowsPerPage) { skip = page * rowsPerPage; } - const checksCount = await Check.countDocuments(checksQuery); - - const checks = await Check.find(checksQuery) - .skip(skip) - .limit(rowsPerPage) - .sort({ createdAt: sortOrder }) - .select(["monitorId", "status", "responseTime", "statusCode", "message"]); - return { checksCount, checks }; + const checks = await Check.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: sortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]); + return checks[0]; } catch (error) { error.service = SERVICE_NAME; - error.method = "getTeamChecks"; + error.method = "getChecksByTeam"; throw error; } }; @@ -298,9 +288,8 @@ const updateChecksTTL = async (teamId, ttl) => { export { createCheck, - getChecksCount, - getChecks, - getTeamChecks, + getChecksByMonitor, + getChecksByTeam, deleteChecks, deleteChecksByTeamId, updateChecksTTL, diff --git a/Server/routes/checkRoute.js b/Server/routes/checkRoute.js index 289e0bc2f..bab45e4d4 100644 --- a/Server/routes/checkRoute.js +++ b/Server/routes/checkRoute.js @@ -11,7 +11,7 @@ class CheckRoutes { } initRoutes() { - this.router.get("/:monitorId", this.checkController.getChecks); + this.router.get("/:monitorId", this.checkController.getChecksByMonitor); this.router.post( "/:monitorId", verifyOwnership(Monitor, "monitorId"), @@ -23,7 +23,7 @@ class CheckRoutes { this.checkController.deleteChecks ); - this.router.get("/team/:teamId", this.checkController.getTeamChecks); + this.router.get("/team/:teamId", this.checkController.getChecksByTeam); this.router.delete( "/team/:teamId", diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 03bd6fef9..54edf39f9 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -131,6 +131,7 @@ class NetworkService { const httpResponse = { monitorId: job.data._id, + teamId: job.data.teamId, type: job.data.type, responseTime, payload: response?.data, diff --git a/Server/service/statusService.js b/Server/service/statusService.js index 5138b73ce..cca443e9e 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -83,10 +83,11 @@ class StatusService { * @returns {Object} The check object. */ buildCheck = (networkResponse) => { - const { monitorId, type, status, responseTime, code, message, payload } = + const { monitorId, teamId, type, status, responseTime, code, message, payload } = networkResponse; const check = { monitorId, + teamId, status, statusCode: code, responseTime, From 9535ffa192f740ea3b5678770e44019cfca706d1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jan 2025 15:31:40 -0800 Subject: [PATCH 06/76] Refactor Pagination component to accept a label to improve reusability. Use Pagination component with Incident table --- .../src/Components/HttpStatusLabel/index.jsx | 2 +- .../Table/TablePagination/index.jsx | 16 ++-- .../Pages/Incidents/IncidentTable/index.jsx | 78 ++++++------------- Client/src/Pages/Incidents/index.css | 11 --- Client/src/Pages/Incidents/index.jsx | 1 - Client/src/Pages/Infrastructure/index.jsx | 5 +- .../src/Pages/Uptime/CreateUptime/index.jsx | 20 ++--- Client/src/Pages/Uptime/Home/index.jsx | 5 +- 8 files changed, 49 insertions(+), 89 deletions(-) delete mode 100644 Client/src/Pages/Incidents/index.css diff --git a/Client/src/Components/HttpStatusLabel/index.jsx b/Client/src/Components/HttpStatusLabel/index.jsx index ca9f70fe8..52676ae7c 100644 --- a/Client/src/Components/HttpStatusLabel/index.jsx +++ b/Client/src/Components/HttpStatusLabel/index.jsx @@ -16,7 +16,7 @@ import { BaseLabel } from "../Label"; const DEFAULT_CODE = 9999; // Default code for unknown status const handleStatusCode = (status) => { - if (status >= 100 && status < 600) { + if (status) { return status; } return DEFAULT_CODE; diff --git a/Client/src/Components/Table/TablePagination/index.jsx b/Client/src/Components/Table/TablePagination/index.jsx index 66707494a..61e89232f 100644 --- a/Client/src/Components/Table/TablePagination/index.jsx +++ b/Client/src/Components/Table/TablePagination/index.jsx @@ -5,11 +5,11 @@ import { TablePaginationActions } from "./Actions"; import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react"; Pagination.propTypes = { - monitorCount: PropTypes.number.isRequired, // Total number of items for pagination. + itemCount: PropTypes.number.isRequired, // Total number of items for pagination. page: PropTypes.number.isRequired, // Current page index. rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page. handleChangePage: PropTypes.func.isRequired, // Function to handle page changes. - handleChangeRowsPerPage: PropTypes.func.isRequired, // Function to handle changes in rows per page. + handleChangeRowsPerPage: PropTypes.func, // Function to handle changes in rows per page. }; const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25]; @@ -18,6 +18,7 @@ const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25]; * Pagination component for table navigation with customized styling and behavior. * * @param {object} props - Component properties. + * @param {string} props.paginationLabel - Label for the pagination. * @param {number} props.monitorCount - Total number of monitors to paginate. * @param {number} props.page - Current page index (0-based). * @param {number} props.rowsPerPage - Number of rows to display per page. @@ -26,7 +27,8 @@ const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25]; * @returns {JSX.Element} The Pagination component. */ function Pagination({ - monitorCount, + paginationLabel, + itemCount, page, rowsPerPage, handleChangePage, @@ -35,7 +37,7 @@ function Pagination({ const theme = useTheme(); const start = page * rowsPerPage + 1; - const end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount); + const end = Math.min(page * rowsPerPage + rowsPerPage, itemCount); const range = `${start} - ${end}`; return ( @@ -51,11 +53,11 @@ function Pagination({ variant="body2" sx={{ opacity: 0.7 }} > - Showing {range} of {monitorCount} monitor(s) + Showing {range} of {itemCount} {paginationLabel} { const uiTimezone = useSelector((state) => state.ui.timezone); @@ -24,18 +23,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { const mode = useSelector((state) => state.ui.mode); const [checks, setChecks] = useState([]); const [checksCount, setChecksCount] = useState(0); - const [paginationController, setPaginationController] = useState({ - page: 0, - rowsPerPage: 14, - }); - const [isLoading, setIsLoading] = useState(false); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); - useEffect(() => { - setPaginationController((prevPaginationController) => ({ - ...prevPaginationController, - page: 0, - })); - }, [filter, selectedMonitor]); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchPage = async () => { @@ -53,8 +44,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { limit: null, dateRange: null, filter: filter, - page: paginationController.page, - rowsPerPage: paginationController.rowsPerPage, + page: page, + rowsPerPage: rowsPerPage, }); } else { res = await networkService.getChecksByMonitor({ @@ -64,8 +55,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { limit: null, dateRange: null, filter: filter, - page: paginationController.page, - rowsPerPage: paginationController.rowsPerPage, + page, + rowsPerPage, }); } setChecks(res.data.data.checks); @@ -77,21 +68,14 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { } }; fetchPage(); - }, [ - authToken, - user, - monitors, - selectedMonitor, - filter, - paginationController.page, - paginationController.rowsPerPage, - ]); + }, [authToken, user, monitors, selectedMonitor, filter, page, rowsPerPage]); const handlePageChange = (_, newPage) => { - setPaginationController({ - ...paginationController, - page: newPage - 1, // 0-indexed - }); + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); }; const headers = [ @@ -134,28 +118,6 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { { id: "message", content: "Message", render: (row) => row.message }, ]; - let paginationComponent = <>; - if (checksCount > paginationController.rowsPerPage) { - paginationComponent = ( - ( - - )} - sx={{ mt: "auto" }} - /> - ); - } - let sharedStyles = { border: 1, borderColor: theme.palette.primary.lowContrast, @@ -198,8 +160,14 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { headers={headers} data={checks} /> - - {paginationComponent} + )} diff --git a/Client/src/Pages/Incidents/index.css b/Client/src/Pages/Incidents/index.css deleted file mode 100644 index ddbb44e6d..000000000 --- a/Client/src/Pages/Incidents/index.css +++ /dev/null @@ -1,11 +0,0 @@ -.incidents h1.MuiTypography-root { - font-size: var(--env-var-font-size-large); - font-weight: 600; -} -.incidents button.MuiButtonBase-root, -.incidents p.MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} -.incidents button.MuiButtonBase-root { - min-height: 34px; -} diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 86f5afc5f..6e3f513f7 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -8,7 +8,6 @@ import { useTheme } from "@emotion/react"; import Select from "../../Components/Inputs/Select"; import IncidentTable from "./IncidentTable"; import SkeletonLayout from "./skeleton"; -import "./index.css"; const Incidents = () => { const theme = useTheme(); diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 08636a05e..0bf367b99 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -13,7 +13,7 @@ import { Box, Button, IconButton, Stack } from "@mui/material"; import Breadcrumbs from "../../Components/Breadcrumbs"; import { StatusLabel } from "../../Components/Label"; import { Heading } from "../../Components/Heading"; -import { Pagination } from "../../Components/Table/TablePagination/index.jsx"; +import Pagination from "../../Components/Table/TablePagination/index.jsx"; // import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice"; import { networkService } from "../../Utils/NetworkService.js"; import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx"; @@ -269,7 +269,8 @@ function Infrastructure() { /> { } if (monitor.type === "http") { - const checkEndpointAction = await dispatch( - checkEndpointResolution({ authToken, monitorURL: form.url }) - ); - if (checkEndpointAction.meta.requestStatus === "rejected") { - createToast({ - body: "The endpoint you entered doesn't resolve. Check the URL again.", - }); - setErrors({ url: "The entered URL is not reachable." }); - return; - } + // const checkEndpointAction = await dispatch( + // checkEndpointResolution({ authToken, monitorURL: form.url }) + // ); + // if (checkEndpointAction.meta.requestStatus === "rejected") { + // createToast({ + // body: "The endpoint you entered doesn't resolve. Check the URL again.", + // }); + // setErrors({ url: "The entered URL is not reachable." }); + // return; + // } } form = { diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index df59185fb..446702ebb 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -5,7 +5,7 @@ import SkeletonLayout from "./skeleton"; import Fallback from "./fallback"; import StatusBox from "./StatusBox"; import UptimeDataTable from "./UptimeDataTable"; -import { Pagination } from "../../../Components/Table/TablePagination"; +import Pagination from "../../../Components/Table/TablePagination"; // Utils import { useTheme } from "@emotion/react"; @@ -225,7 +225,8 @@ const UptimeMonitors = () => { triggerUpdate={triggerUpdate} /> Date: Tue, 21 Jan 2025 15:39:49 -0800 Subject: [PATCH 07/76] Add default key to table rows --- Client/src/Components/Table/index.jsx | 3 ++- Client/src/Pages/Incidents/IncidentTable/index.jsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx index d8eddcc24..3d170f440 100644 --- a/Client/src/Components/Table/index.jsx +++ b/Client/src/Components/Table/index.jsx @@ -82,9 +82,10 @@ const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => { ) : ( data.map((row) => { + const key = row.id || row._id || Math.random(); return ( config?.onRowClick(row)} > diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index ca40fe66e..ab7af33ca 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -59,6 +59,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { rowsPerPage, }); } + setChecks(res.data.data.checks); setChecksCount(res.data.data.checksCount); } catch (error) { From 9e9f5d435dd91bc77f12e819277ea4f06a49ed3a Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:20:34 -0500 Subject: [PATCH 08/76] Update index.jsx --- Client/src/Pages/Settings/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index 2f7c12a0d..3cabac713 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -266,7 +266,7 @@ const Settings = () => { open={isOpen.deleteStats} theme={theme} title="Do you want to clear all stats?" - description="Once deleted, this monitor cannot be retrieved." + description="Once deleted, your monitors cannot be retrieved." onCancel={() => setIsOpen(deleteStatsMonitorsInitState)} confirmationButtonLabel="Yes, clear all stats" onConfirm={handleClearStats} @@ -279,7 +279,7 @@ const Settings = () => { Demo monitors - Here you can add and remove demo monitors + Here you can add and remove demo monitors. From c7b65e537a714d70440ec0dd21de69ed6234f4a4 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:02:17 -0500 Subject: [PATCH 09/76] Update roadmap --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c26fb399f..7294d6a81 100644 --- a/README.md +++ b/README.md @@ -58,15 +58,14 @@ If you have any questions, suggestions or comments, please use our [Discord chan **Short term roadmap:** - Port monitoring (**complete**, waiting to be deployed to stable version) https://github.com/bluewave-labs/Checkmate/issues/1476 -- Global (distributed) uptime checking on Solana network (**in progress**) +- Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593 - Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131 - Translations (i18n) (**in progress**) -- Better notification options (Webhooks, Discord, Telegram, Slack) https://github.com/bluewave-labs/Checkmate/issues/1545 -- Server security monitoring -- Command line interface (CLI) https://github.com/bluewave-labs/Checkmate/issues/1558 +- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545 +- Command line interface (CLI) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1558 - JSON query monitoring https://github.com/bluewave-labs/Checkmate/issues/1573 -- More configuration options - Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546 +- More configuration options - DNS monitoring ## 🏗️ Screenshots From fcd28afc97d02d9c6bcb387d5fe868abe9be0a46 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Wed, 22 Jan 2025 01:38:07 -0500 Subject: [PATCH 10/76] Add github trending #1 badge --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7294d6a81..c37adfc6a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -## **Announcing $5000 grant funding in partnership with [UpRock](https://uprock.com) to build distributed uptime monitoring - check [our web page](https://checkmate.so) for funding ideas** - -![Frame 34](https://github.com/user-attachments/assets/4ecf2d6e-b3f9-47e9-9e31-b9cb8f3f00b9) + + ![](https://img.shields.io/github/license/bluewave-labs/checkmate) ![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate) ![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate) @@ -12,6 +11,7 @@ ![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901) +

Checkmate

An open source uptime and infrastructure monitoring application

From 00e1286f24597d3832b975263831f118f4cd545e Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 22 Jan 2025 13:04:14 +0000 Subject: [PATCH 11/76] fix(deps): update dependency bullmq to v5.35.0 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index a9632c82e..bbc7460fb 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.34.10", + "bullmq": "5.35.0", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", @@ -1748,9 +1748,9 @@ } }, "node_modules/bullmq": { - "version": "5.34.10", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.10.tgz", - "integrity": "sha512-ia6EzpQm1ZPq6GUBSLyfvzJrhdBTd1f3Gn2g9pFtLX4hBOob6QHmcmBzGgPlSCyr/i2Qfe4OdjS21bRd02srbw==", + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.35.0.tgz", + "integrity": "sha512-4wOFPv5oVkcK5A11cwsk5Zzc7Hz7xFe2rHIQfsg0UmWjtwTj/nc7+vPjcMGML6jdl7WejuHXnMeop4uxxo/3WA==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", diff --git a/Server/package.json b/Server/package.json index 3326ab248..e0f66e6df 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.34.10", + "bullmq": "5.35.0", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", From 90e697a1bdf776b130f8b34a1fb061fd53bb205d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 22 Jan 2025 14:57:22 +0000 Subject: [PATCH 12/76] chore(deps): update dependency mocha to v11.1.0 --- Server/package-lock.json | 116 ++------------------------------------- Server/package.json | 2 +- 2 files changed, 7 insertions(+), 111 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index bbc7460fb..2659edbab 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -40,7 +40,7 @@ "eslint-plugin-mocha": "^10.5.0", "esm": "3.2.25", "globals": "^15.14.0", - "mocha": "11.0.1", + "mocha": "11.1.0", "ngrok": "5.0.0-beta.2", "nodemon": "3.1.9", "prettier": "^3.3.3", @@ -5450,9 +5450,9 @@ "license": "MIT" }, "node_modules/mocha": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz", - "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", "dev": true, "license": "MIT", "dependencies": { @@ -5473,8 +5473,8 @@ "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { @@ -5485,35 +5485,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/mocha/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/mocha/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/mocha/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -5527,34 +5498,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -5571,53 +5514,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/mongodb": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", diff --git a/Server/package.json b/Server/package.json index e0f66e6df..c4ef0958f 100644 --- a/Server/package.json +++ b/Server/package.json @@ -47,7 +47,7 @@ "eslint-plugin-mocha": "^10.5.0", "esm": "3.2.25", "globals": "^15.14.0", - "mocha": "11.0.1", + "mocha": "11.1.0", "ngrok": "5.0.0-beta.2", "nodemon": "3.1.9", "prettier": "^3.3.3", From 9a8ce41c95094316f40c7a5103d1313a22533c5c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 07:06:53 -0800 Subject: [PATCH 13/76] add db utils for seeding db --- Server/db/mongo/utils/seedDb.js | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Server/db/mongo/utils/seedDb.js diff --git a/Server/db/mongo/utils/seedDb.js b/Server/db/mongo/utils/seedDb.js new file mode 100644 index 000000000..2d9ea4df8 --- /dev/null +++ b/Server/db/mongo/utils/seedDb.js @@ -0,0 +1,42 @@ +import Monitor from "../models/Monitor"; +import Check from "../models/Check"; + +const generateRandomUrl = () => { + const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; + const paths = ["api", "status", "health", "ping", "check"]; + return `https://${domains[Math.floor(Math.random() * domains.length)]}/${paths[Math.floor(Math.random() * paths.length)]}`; +}; + +const generateChecks = (monitorId, count) => { + const checks = []; + const endTime = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago + const startTime = new Date(endTime - count * 60 * 1000); // count minutes before endTime + + for (let i = 0; i < count; i++) { + const timestamp = new Date(startTime.getTime() + i * 60 * 1000); + const status = Math.random() > 0.05; // 95% chance of being up + + checks.push({ + monitorId, + status, + responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms + createdAt: timestamp, + updatedAt: timestamp, + }); + } + + return checks; +}; + +const seedDb = async () => { + try { + await Monitor.deleteMany({}); + await Check.deleteMany({}); + } catch (error) { + console.error(error); + } + + for (let i = 0; i < 300; i++) { + const monitor = await Monitor.create({}); + } +}; From 9ffe17e9ebd82b7ecf27f70ded8126e673188c88 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 08:29:38 -0800 Subject: [PATCH 14/76] add db utils --- Server/controllers/monitorController.js | 10 ++++++++- Server/db/models/Check.js | 1 + Server/db/mongo/modules/checkModule.js | 26 +++++++++++++++++---- Server/db/mongo/utils/seedDb.js | 30 ++++++++++++++++++++----- Server/index.js | 3 +++ Server/routes/monitorRoute.js | 2 ++ 6 files changed, 61 insertions(+), 11 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 097172854..5b880f2c6 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -20,7 +20,7 @@ import { getTokenFromHeaders } from "../utils/utils.js"; import logger from "../utils/logger.js"; import { handleError, handleValidationError } from "./controllerUtils.js"; import axios from "axios"; - +import seedDb from "../db/mongo/utils/seedDb.js"; const SERVICE_NAME = "monitorController"; class MonitorController { @@ -495,6 +495,14 @@ class MonitorController { next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId")); } }; + + seedDb = async (req, res, next) => { + try { + await seedDb(userId, teamId); + } catch (error) { + next(handleError(error, SERVICE_NAME, "seedDb")); + } + }; } export default MonitorController; diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index dba47a12e..af9f797b2 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -76,6 +76,7 @@ const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: tr CheckSchema.index({ createdAt: 1 }); CheckSchema.index({ monitorId: 1, createdAt: 1 }); CheckSchema.index({ monitorId: 1, createdAt: -1 }); +CheckSchema.index({ teamId: 1, createdAt: -1 }); export default mongoose.model("Check", CheckSchema); export { BaseCheckSchema }; diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 870011d9f..edc70e5d6 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -68,10 +68,9 @@ const createCheck = async (checkData) => { const getChecksByMonitor = async (req) => { try { const { monitorId } = req.params; - let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; + let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - // Match const matchStage = { monitorId: ObjectId.createFromHexString(monitorId), @@ -138,10 +137,10 @@ const getChecksByMonitor = async (req) => { const getChecksByTeam = async (req) => { try { - let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; + let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - + !dateRange && (dateRange = "day"); const { teamId } = req.params; const matchStage = { teamId: ObjectId.createFromHexString(teamId), @@ -184,6 +183,7 @@ const getChecksByTeam = async (req) => { const checks = await Check.aggregate([ { $match: matchStage }, { $sort: { createdAt: sortOrder } }, + { $facet: { summary: [{ $count: "checksCount" }], @@ -197,6 +197,24 @@ const getChecksByTeam = async (req) => { }, }, ]); + + const queryPlan = await Check.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: sortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]).explain("executionStats"); + console.log(queryPlan); return checks[0]; } catch (error) { error.service = SERVICE_NAME; diff --git a/Server/db/mongo/utils/seedDb.js b/Server/db/mongo/utils/seedDb.js index 2d9ea4df8..b67276027 100644 --- a/Server/db/mongo/utils/seedDb.js +++ b/Server/db/mongo/utils/seedDb.js @@ -1,5 +1,8 @@ -import Monitor from "../models/Monitor"; -import Check from "../models/Check"; +import Monitor from "../../models/Monitor.js"; +import Check from "../../models/Check.js"; +import { ObjectId } from "mongodb"; +const FAKE_TEAM_ID = "67034bceb4c4ea3e8f75fa93"; +const FAKE_USER_ID = "67034bceb4c4ea3e8f75fa95"; const generateRandomUrl = () => { const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; @@ -18,6 +21,7 @@ const generateChecks = (monitorId, count) => { checks.push({ monitorId, + teamId: FAKE_TEAM_ID, status, responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms createdAt: timestamp, @@ -30,13 +34,27 @@ const generateChecks = (monitorId, count) => { const seedDb = async () => { try { + console.log("Deleting all monitors and checks"); await Monitor.deleteMany({}); await Check.deleteMany({}); + console.log("Adding monitors"); + for (let i = 0; i < 300; i++) { + const monitor = await Monitor.create({ + name: `Monitor ${i}`, + url: generateRandomUrl(), + type: "http", + userId: FAKE_USER_ID, + teamId: FAKE_TEAM_ID, + interval: 60000, + active: true, + }); + console.log(`Adding monitor and checks for monitor ${i}`); + const checks = generateChecks(monitor._id, 10000); + await Check.insertMany(checks); + } } catch (error) { console.error(error); } - - for (let i = 0; i < 300; i++) { - const monitor = await Monitor.create({}); - } }; + +export default seedDb; diff --git a/Server/index.js b/Server/index.js index 7a4129bc9..a4b67386e 100644 --- a/Server/index.js +++ b/Server/index.js @@ -74,6 +74,8 @@ import MongoDB from "./db/mongo/MongoDB.js"; import IORedis from "ioredis"; +import seedDb from "./db/mongo/utils/seedDb.js"; + const SERVICE_NAME = "Server"; const SHUTDOWN_TIMEOUT = 1000; let isShuttingDown = false; @@ -262,6 +264,7 @@ const startApp = async () => { ); // Init job queue await jobQueue.initJobQueue(); + // await seedDb(); // Middleware app.use( cors() diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index 92b529417..964bbee7a 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -75,6 +75,8 @@ class MonitorRoutes { isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors ); + + this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb); } getRouter() { From 2a74f2e8dacf3d2ec42a71c015908e62ccd55feb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:06:39 -0800 Subject: [PATCH 15/76] remove seed from index --- Server/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/Server/index.js b/Server/index.js index a4b67386e..7a4129bc9 100644 --- a/Server/index.js +++ b/Server/index.js @@ -74,8 +74,6 @@ import MongoDB from "./db/mongo/MongoDB.js"; import IORedis from "ioredis"; -import seedDb from "./db/mongo/utils/seedDb.js"; - const SERVICE_NAME = "Server"; const SHUTDOWN_TIMEOUT = 1000; let isShuttingDown = false; @@ -264,7 +262,6 @@ const startApp = async () => { ); // Init job queue await jobQueue.initJobQueue(); - // await seedDb(); // Middleware app.use( cors() From fc36da4f7b65b57c0d39636d00345ff935461f1a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:07:32 -0800 Subject: [PATCH 16/76] Add hour filters to check queries --- Server/db/models/Check.js | 1 + Server/db/mongo/modules/checkModule.js | 29 +++++++------------------- Server/db/mongo/utils/seedDb.js | 17 +++++++-------- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index af9f797b2..158749986 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -77,6 +77,7 @@ CheckSchema.index({ createdAt: 1 }); CheckSchema.index({ monitorId: 1, createdAt: 1 }); CheckSchema.index({ monitorId: 1, createdAt: -1 }); CheckSchema.index({ teamId: 1, createdAt: -1 }); +CheckSchema.index({ teamId: 1 }); export default mongoose.model("Check", CheckSchema); export { BaseCheckSchema }; diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index edc70e5d6..ec98cf63c 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -6,9 +6,11 @@ import { ObjectId } from "mongodb"; const SERVICE_NAME = "checkModule"; const dateRangeLookup = { + hour: new Date(new Date().setHours(new Date().getHours() - 1)), day: new Date(new Date().setDate(new Date().getDate() - 1)), week: new Date(new Date().setDate(new Date().getDate() - 7)), month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: undefined, }; /** @@ -75,7 +77,7 @@ const getChecksByMonitor = async (req) => { const matchStage = { monitorId: ObjectId.createFromHexString(monitorId), status: false, - ...(dateRange && { + ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], }, @@ -140,18 +142,18 @@ const getChecksByTeam = async (req) => { let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - !dateRange && (dateRange = "day"); + console.log(dateRangeLookup[dateRange]); const { teamId } = req.params; const matchStage = { teamId: ObjectId.createFromHexString(teamId), status: false, - ...(dateRange && { + ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], }, }), }; - + console.log(matchStage); // Add filter to match stage if (filter !== undefined) { switch (filter) { @@ -166,7 +168,7 @@ const getChecksByTeam = async (req) => { logger.warn({ message: "invalid filter", service: SERVICE_NAME, - method: "getTeamChecks", + method: "getChecksByTeam", }); break; } @@ -198,23 +200,6 @@ const getChecksByTeam = async (req) => { }, ]); - const queryPlan = await Check.aggregate([ - { $match: matchStage }, - { $sort: { createdAt: sortOrder } }, - { - $facet: { - summary: [{ $count: "checksCount" }], - checks: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, - checks: "$checks", - }, - }, - ]).explain("executionStats"); - console.log(queryPlan); return checks[0]; } catch (error) { error.service = SERVICE_NAME; diff --git a/Server/db/mongo/utils/seedDb.js b/Server/db/mongo/utils/seedDb.js index b67276027..cd4abb8d5 100644 --- a/Server/db/mongo/utils/seedDb.js +++ b/Server/db/mongo/utils/seedDb.js @@ -1,8 +1,5 @@ import Monitor from "../../models/Monitor.js"; import Check from "../../models/Check.js"; -import { ObjectId } from "mongodb"; -const FAKE_TEAM_ID = "67034bceb4c4ea3e8f75fa93"; -const FAKE_USER_ID = "67034bceb4c4ea3e8f75fa95"; const generateRandomUrl = () => { const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; @@ -10,7 +7,7 @@ const generateRandomUrl = () => { return `https://${domains[Math.floor(Math.random() * domains.length)]}/${paths[Math.floor(Math.random() * paths.length)]}`; }; -const generateChecks = (monitorId, count) => { +const generateChecks = (monitorId, teamId, count) => { const checks = []; const endTime = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago const startTime = new Date(endTime - count * 60 * 1000); // count minutes before endTime @@ -21,7 +18,7 @@ const generateChecks = (monitorId, count) => { checks.push({ monitorId, - teamId: FAKE_TEAM_ID, + teamId, status, responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms createdAt: timestamp, @@ -32,7 +29,7 @@ const generateChecks = (monitorId, count) => { return checks; }; -const seedDb = async () => { +const seedDb = async (userId, teamId) => { try { console.log("Deleting all monitors and checks"); await Monitor.deleteMany({}); @@ -43,13 +40,13 @@ const seedDb = async () => { name: `Monitor ${i}`, url: generateRandomUrl(), type: "http", - userId: FAKE_USER_ID, - teamId: FAKE_TEAM_ID, + userId, + teamId, interval: 60000, - active: true, + active: false, }); console.log(`Adding monitor and checks for monitor ${i}`); - const checks = generateChecks(monitor._id, 10000); + const checks = generateChecks(monitor._id, teamId, 10000); await Check.insertMany(checks); } } catch (error) { From f46f8cb12b40bbaa4638fbbf998c4d4f70e0b38d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:07:43 -0800 Subject: [PATCH 17/76] Add hour filter to incidents page --- Server/controllers/monitorController.js | 5 ++++- Server/validation/joi.js | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 5b880f2c6..86c0fb840 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -498,7 +498,10 @@ class MonitorController { seedDb = async (req, res, next) => { try { - await seedDb(userId, teamId); + const token = getTokenFromHeaders(req.headers); + const { jwtSecret } = this.settingsService.getSettings(); + const { _id, teamId } = jwt.verify(token, jwtSecret); + await seedDb(_id, teamId); } catch (error) { next(handleError(error, SERVICE_NAME, "seedDb")); } diff --git a/Server/validation/joi.js b/Server/validation/joi.js index d30235048..8f366a113 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -131,7 +131,7 @@ const getMonitorByIdQueryValidation = joi.object({ status: joi.boolean(), sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - dateRange: joi.string().valid("day", "week", "month"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), numToDisplay: joi.number(), normalize: joi.boolean(), }); @@ -166,7 +166,7 @@ const getMonitorStatsByIdQueryValidation = joi.object({ status: joi.string(), limit: joi.number(), sortOrder: joi.string().valid("asc", "desc"), - dateRange: joi.string().valid("day", "week", "month", "all"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), numToDisplay: joi.number(), normalize: joi.boolean(), }); @@ -217,7 +217,7 @@ const getHardwareDetailsByIdParamValidation = joi.object({ }); const getHardwareDetailsByIdQueryValidation = joi.object({ - dateRange: joi.string().valid("day", "week", "month"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), }); //**************************************** @@ -288,7 +288,7 @@ const getChecksParamValidation = joi.object({ const getChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - dateRange: joi.string().valid("day", "week", "month"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), filter: joi.string().valid("all", "down", "resolve"), page: joi.number(), rowsPerPage: joi.number(), @@ -301,7 +301,7 @@ const getTeamChecksParamValidation = joi.object({ const getTeamChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - dateRange: joi.string().valid("day", "week", "month"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), filter: joi.string().valid("all", "down", "resolve"), page: joi.number(), rowsPerPage: joi.number(), From 4268966b44066dfa2d325fdd8870c389e6c683af Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:10:09 -0800 Subject: [PATCH 18/76] Add hour filter to incidents page --- .../Pages/Incidents/IncidentTable/index.jsx | 10 +- Client/src/Pages/Incidents/index.jsx | 160 +++++++++++++----- 2 files changed, 121 insertions(+), 49 deletions(-) diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index ab7af33ca..e3ddba24a 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -15,7 +15,8 @@ import { Empty } from "./Empty/Empty"; import { IncidentSkeleton } from "./Skeleton/Skeleton"; import DataTable from "../../../Components/Table"; import Pagination from "../../../Components/Table/TablePagination"; -const IncidentTable = ({ monitors, selectedMonitor, filter }) => { + +const IncidentTable = ({ monitors, selectedMonitor, filter, dateRange }) => { const uiTimezone = useSelector((state) => state.ui.timezone); const theme = useTheme(); @@ -25,7 +26,6 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { const [checksCount, setChecksCount] = useState(0); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); - const [isLoading, setIsLoading] = useState(false); useEffect(() => { @@ -42,7 +42,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { teamId: user.teamId, sortOrder: "desc", limit: null, - dateRange: null, + dateRange, filter: filter, page: page, rowsPerPage: rowsPerPage, @@ -53,7 +53,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { monitorId: selectedMonitor, sortOrder: "desc", limit: null, - dateRange: null, + dateRange, filter: filter, page, rowsPerPage, @@ -69,7 +69,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { } }; fetchPage(); - }, [authToken, user, monitors, selectedMonitor, filter, page, rowsPerPage]); + }, [authToken, user, monitors, selectedMonitor, filter, page, rowsPerPage, dateRange]); const handlePageChange = (_, newPage) => { setPage(newPage); diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 6e3f513f7..0256b89b3 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -20,6 +20,7 @@ const Incidents = () => { // TODO do something with these filters const [filter, setFilter] = useState("all"); + const [dateRange, setDateRange] = useState("hour"); useEffect(() => { const fetchMonitors = async () => { @@ -67,6 +68,7 @@ const Incidents = () => { return ( @@ -76,62 +78,132 @@ const Incidents = () => { <> - - Incidents for - - + + - - - + + + + + + - Cannot Resolve - - + Limit by: + + + + + + + + )} From 15cd3b23c39701525498272aacf43fe4bad01ff6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:13:44 -0800 Subject: [PATCH 19/76] Add daterange prop --- Client/src/Pages/Incidents/IncidentTable/index.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index e3ddba24a..ae9a3ae2a 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -179,6 +179,7 @@ IncidentTable.propTypes = { monitors: PropTypes.object.isRequired, selectedMonitor: PropTypes.string.isRequired, filter: PropTypes.string.isRequired, + dateRange: PropTypes.string.isRequired, }; export default IncidentTable; From 1b31f9ac4cc6adbe6ebd6ecb4c6dfe3b97917e55 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:36:27 -0800 Subject: [PATCH 20/76] Remove console logs, update UI --- Client/src/Pages/Incidents/index.jsx | 11 +++++------ Server/db/mongo/modules/checkModule.js | 2 -- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 0256b89b3..8305ec672 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -68,7 +68,6 @@ const Incidents = () => { return ( @@ -144,7 +143,7 @@ const Incidents = () => { filled={(filter === "resolve").toString()} onClick={() => setFilter("resolve")} > - Cannot Resolve + Cannot resolve @@ -158,7 +157,7 @@ const Incidents = () => { component="h1" color={theme.palette.primary.contrastTextSecondary} > - Limit by: + Show: { filled={(dateRange === "hour").toString()} onClick={() => setDateRange("hour")} > - Hour + Last hour - - - - - ); -}; - -TablePaginationActions.propTypes = { - count: PropTypes.number.isRequired, - page: PropTypes.number.isRequired, - rowsPerPage: PropTypes.number.isRequired, - onPageChange: PropTypes.func.isRequired, -}; const MaintenanceTable = ({ page, @@ -111,13 +34,102 @@ const MaintenanceTable = ({ updateCallback, }) => { const { rowsPerPage } = useSelector((state) => state.ui.maintenance); - const theme = useTheme(); + console.log(rowsPerPage); const dispatch = useDispatch(); const handleChangePage = (event, newPage) => { setPage(newPage); }; + const handleChangeRowsPerPage = (event) => { + dispatch( + setRowsPerPage({ + value: parseInt(event.target.value, 10), + table: "maintenance", + }) + ); + setPage(0); + }; + + const headers = [ + { + id: "name", + content: ( + handleSort("name")}> + Maintenance Window Name + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + ), + render: (row) => row.name, + }, + { + id: "status", + content: ( + handleSort("status")}> + {" "} + Status + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + ), + render: (row) => { + const status = row.active ? "up" : "paused"; + const text = row.active ? "active" : "paused"; + + return ( + + ); + }, + }, + { + id: "nextWindow", + content: "Next window", + render: (row) => { + return getTimeToNextWindow(row.start, row.end, row.repeat); + }, + }, + { + id: "repeat", + content: "Repeat", + render: (row) => { + return row.repeat === 0 ? "N/A" : formatDurationRounded(row.repeat); + }, + }, + { + id: "actions", + content: "Actions", + render: (row) => ( + + ), + }, + ]; + const getTimeToNextWindow = (startTime, endTime, repeat) => { //1. Advance time closest to next window as possible const now = dayjs(); @@ -153,26 +165,6 @@ const MaintenanceTable = ({ } }; - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: "maintenance", - }) - ); - setPage(0); - }; - - /** - * Helper function to calculate the range of displayed rows. - * @returns {string} - */ - const getRange = () => { - let start = page * rowsPerPage + 1; - let end = Math.min(page * rowsPerPage + rowsPerPage, maintenanceWindowCount); - return `${start} - ${end}`; - }; - const handleSort = async (field) => { let order = ""; if (sort.field !== field) { @@ -183,165 +175,21 @@ const MaintenanceTable = ({ setSort({ field, order }); }; + console.log(handleChangePage); + return ( <> - - - - - handleSort("name")} - > - - Maintenance Window Name - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - - handleSort("status")} - > - {" "} - - {" "} - Status - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - - Next Window - Repeat - Actions - - - - {maintenanceWindows.map((maintenanceWindow) => { - const text = maintenanceWindow.active ? "active" : "paused"; - const status = maintenanceWindow.active ? "up" : "paused"; - return ( - - {maintenanceWindow.name} - - - - - {getTimeToNextWindow( - maintenanceWindow.start, - maintenanceWindow.end, - maintenanceWindow.repeat - )} - - - {maintenanceWindow.repeat === 0 - ? "N/A" - : formatDurationRounded(maintenanceWindow.repeat)} - - - - - - ); - })} - -
-
- - - Showing {getRange()} of {maintenanceWindowCount} maintenance window(s) - - - `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}` - } - slotProps={{ - select: { - MenuProps: { - keepMounted: true, - disableScrollLock: true, - PaperProps: { - className: "pagination-dropdown", - sx: { - mt: 0, - mb: theme.spacing(2), - }, - }, - transformOrigin: { vertical: "bottom", horizontal: "left" }, - anchorOrigin: { vertical: "top", horizontal: "left" }, - sx: { mt: theme.spacing(-2) }, - }, - inputProps: { id: "pagination-dropdown" }, - IconComponent: SelectorVertical, - sx: { - ml: theme.spacing(4), - mr: theme.spacing(12), - minWidth: theme.spacing(20), - textAlign: "left", - "&.Mui-focused > div": { - backgroundColor: theme.palette.primary.main, - }, - }, - }, - }} - sx={{ - mt: theme.spacing(6), - color: theme.palette.primary.contrastTextSecondary, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - strokeWidth: 1.3, - }, - "& .MuiSelect-select": { - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - }, - }} - /> - + + ); }; From ffffda72821eb8392cd8d0076ea6e896dcc5be4d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 23 Jan 2025 09:14:59 +0000 Subject: [PATCH 22/76] fix(deps): update dependency nodemailer to v6.10.0 --- Server/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 2659edbab..197c7b52b 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -5848,9 +5848,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", "license": "MIT-0", "engines": { "node": ">=6.0.0" From 01592cf0f01626e40fef5bbd4acb6eb001f56f59 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 23 Jan 2025 13:37:56 +0000 Subject: [PATCH 23/76] fix(deps): update dependency bullmq to v5.35.1 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 2659edbab..ee232f4ef 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.35.0", + "bullmq": "5.35.1", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", @@ -1748,9 +1748,9 @@ } }, "node_modules/bullmq": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.35.0.tgz", - "integrity": "sha512-4wOFPv5oVkcK5A11cwsk5Zzc7Hz7xFe2rHIQfsg0UmWjtwTj/nc7+vPjcMGML6jdl7WejuHXnMeop4uxxo/3WA==", + "version": "5.35.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.35.1.tgz", + "integrity": "sha512-LnzqisWyF4V8KhgFnK7iaJdQn9Q61EdHdCT6sVTzA3jZr6CHxw/OBmBZhzA8Xq4bsvsmvdjr8pmGftC78aWgXQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", diff --git a/Server/package.json b/Server/package.json index c4ef0958f..873f8ce67 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.35.0", + "bullmq": "5.35.1", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", From 2c1e8540043f586c4dc7ae30bced24ab669dfd8d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 23 Jan 2025 07:25:56 -0800 Subject: [PATCH 24/76] reduce size of demo monitors --- Server/utils/demoMonitors.json | 1269 +--------------------------- Server/utils/demoMonitorsOld.json | 1271 +++++++++++++++++++++++++++++ package-lock.json | 6 + 3 files changed, 1287 insertions(+), 1259 deletions(-) create mode 100644 Server/utils/demoMonitorsOld.json create mode 100644 package-lock.json diff --git a/Server/utils/demoMonitors.json b/Server/utils/demoMonitors.json index ffce7d226..3efea8d0e 100644 --- a/Server/utils/demoMonitors.json +++ b/Server/utils/demoMonitors.json @@ -1,1271 +1,22 @@ [ { - "name": "0to255", - "url": "https://0to255.com" + "name": "Google", + "url": "https://www.google.com" }, { - "name": "10015.io", - "url": "https://10015.io" + "name": "Facebook", + "url": "https://www.facebook.com" }, { - "name": "3DIcons", - "url": "https://3dicons.co" + "name": "Yahoo", + "url": "https://www.yahoo.com" }, { - "name": "About.me", - "url": "https://about.me" + "name": "Amazon", + "url": "https://www.amazon.com" }, { - "name": "Alias", - "url": "https://alias.co" - }, - { - "name": "All About Berlin", - "url": "https://allaboutberlin.com" - }, - { - "name": "All Acronyms", - "url": "https://allacronyms.com" - }, - { - "name": "All You Can Read ", - "url": "https://allyoucanread.com" - }, - { - "name": "AllTrails", - "url": "https://alltrails.com" - }, - { - "name": "Anotepad", - "url": "https://anotepad.com" - }, - { - "name": "AnswerSocrates", - "url": "https://answersocrates.com" - }, - { - "name": "AnswerThePublic ", - "url": "https://answerthepublic.com" - }, - { - "name": "Apollo ", - "url": "https://apollo.io" - }, - { - "name": "ArrayList", - "url": "https://arraylist.org" - }, - { - "name": "Ask Difference", - "url": "https://askdifference.com" - }, - { - "name": "Audd.io", - "url": "https://audd.io" - }, - { - "name": "Audiocheck", - "url": "https://audiocheck.net" - }, - { - "name": "Audionautix", - "url": "https://audionautix.com" - }, - { - "name": "Authentic Jobs", - "url": "https://authenticjobs.com" - }, - { - "name": "Behind the Name", - "url": "https://behindthename.com" - }, - { - "name": "Bilim Terimleri", - "url": "https://terimler.org" - }, - { - "name": "BitBof", - "url": "https://bitbof.com" - }, - { - "name": "Blank Page", - "url": "https://blank.page" - }, - { - "name": "Bonanza", - "url": "https://bonanza.com" - }, - { - "name": "BookCrossing", - "url": "https://bookcrossing.com" - }, - { - "name": "Browse AI", - "url": "https://browse.ai" - }, - { - "name": "Bubbl.us", - "url": "https://bubbl.us" - }, - { - "name": "Business Model Toolbox", - "url": "https://bmtoolbox.net" - }, - { - "name": "ByClickDownloader", - "url": "https://byclickdownloader.com" - }, - { - "name": "Calligraphr", - "url": "https://calligraphr.com" - }, - { - "name": "CertificateClaim", - "url": "https://certificateclaim.com" - }, - { - "name": "Chosic", - "url": "https://chosic.com" - }, - { - "name": "ClipDrop", - "url": "https://clipdrop.co" - }, - { - "name": "CloudConvert", - "url": "https://cloudconvert.com" - }, - { - "name": "CodingFont", - "url": "https://codingfont.com" - }, - { - "name": "Color Hunt", - "url": "https://colorhunt.co" - }, - { - "name": "ColorHexa", - "url": "https://colorhexa.com" - }, - { - "name": "Conversion-Tool", - "url": "https://conversion-tool.com" - }, - { - "name": "Cool Startup Jobs", - "url": "https://coolstartupjobs.com" - }, - { - "name": "Coroflot", - "url": "https://coroflot.com" - }, - { - "name": "Corrupt-a-File", - "url": "https://corrupt-a-file.net" - }, - { - "name": "Couchsurfing", - "url": "https://couchsurfing.com" - }, - { - "name": "Countries Been", - "url": "https://countriesbeen.com" - }, - { - "name": "Country Code", - "url": "https://countrycode.org" - }, - { - "name": "Creately", - "url": "https://creately.com" - }, - { - "name": "Creately ", - "url": "https://creately.com" - }, - { - "name": "Crossfade.io", - "url": "https://crossfade.io" - }, - { - "name": "Crunchbase", - "url": "https://crunchbase.com" - }, - { - "name": "CVmkr", - "url": "https://cvwizard.com" - }, - { - "name": "Daily Remote", - "url": "https://dailyremote.com" - }, - { - "name": "David Li", - "url": "https://david.li" - }, - { - "name": "DemandHunt", - "url": "https://demandhunt.com" - }, - { - "name": "Designify", - "url": "https://designify.com" - }, - { - "name": "Diff Checker", - "url": "https://diffchecker.com" - }, - { - "name": "DifferenceBetween.info", - "url": "https://differencebetween.info" - }, - { - "name": "Digital Glossary", - "url": "https://digital-glossary.com" - }, - { - "name": "Dimensions", - "url": "https://dimensions.com" - }, - { - "name": "Discoverify Music", - "url": "https://discoverifymusic.com" - }, - { - "name": "discu.eu", - "url": "https://discu.eu" - }, - { - "name": "Do It Yourself", - "url": "https://doityourself.com" - }, - { - "name": "draw.io", - "url": "https://drawio.com" - }, - { - "name": "Drumeo", - "url": "https://drumeo.com" - }, - { - "name": "Dummies", - "url": "https://dummies.com" - }, - { - "name": "Easel.ly", - "url": "https://easel.ly" - }, - { - "name": "Educalingo", - "url": "https://educalingo.com" - }, - { - "name": "Emoji Combos", - "url": "https://emojicombos.com" - }, - { - "name": "EquityBee", - "url": "https://equitybee.com" - }, - { - "name": "EquityZen", - "url": "https://equityzen.com" - }, - { - "name": "Escape Room Tips", - "url": "https://escaperoomtips.com" - }, - { - "name": "Every Noise", - "url": "https://everynoise.com" - }, - { - "name": "Every Time Zone", - "url": "https://everytimezone.com" - }, - { - "name": "Excalideck", - "url": "https://excalideck.com" - }, - { - "name": "Excalidraw", - "url": "https://excalidraw.com" - }, - { - "name": "Extract pics", - "url": "https://extract.pics" - }, - { - "name": "EZGIF", - "url": "https://ezgif.com" - }, - { - "name": "FactSlides", - "url": "https://factslides.com" - }, - { - "name": "FIGR ", - "url": "https://figr.app" - }, - { - "name": "Fine Dictionary", - "url": "https://finedictionary.com" - }, - { - "name": "Fiverr", - "url": "https://fiverr.com" - }, - { - "name": "Fix It Club", - "url": "https://fixitclub.com" - }, - { - "name": "Flightradar24", - "url": "https://flightradar24.com" - }, - { - "name": "FlowCV ", - "url": "https://flowcv.com" - }, - { - "name": "Font Squirrel", - "url": "https://fontsquirrel.com" - }, - { - "name": "FontAwesome", - "url": "https://fontawesome.com" - }, - { - "name": "Fontello ", - "url": "https://fontello.com" - }, - { - "name": "Form to Chatbot", - "url": "https://formtochatbot.com" - }, - { - "name": "Founder Resources", - "url": "https://founderresources.io" - }, - { - "name": "Franz", - "url": "https://meetfranz.com" - }, - { - "name": "Fraze It", - "url": "https://fraze.it" - }, - { - "name": "Freecycle", - "url": "https://freecycle.org" - }, - { - "name": "FreeType", - "url": "https://freetype.org" - }, - { - "name": "FutureM", - "url": "https://futureme.org" - }, - { - "name": "Generated.Photos", - "url": "https://generated.photos" - }, - { - "name": "Get Human", - "url": "https://gethuman.com" - }, - { - "name": "Go Bento", - "url": "https://gobento.com" - }, - { - "name": "Good CV", - "url": "https://goodcv.com" - }, - { - "name": "Grammar Monster", - "url": "https://grammar-monster.com" - }, - { - "name": "Grammar Book", - "url": "https://grammarbook.com" - }, - { - "name": "Gummy Search", - "url": "https://gummysearch.com" - }, - { - "name": "Gumroad", - "url": "https://gumroad.com" - }, - { - "name": "HealthIcons", - "url": "https://healthicons.org" - }, - { - "name": "HexColor", - "url": "https://hexcolor.co" - }, - { - "name": "Hidden Life Radio", - "url": "https://hiddenliferadio.com" - }, - { - "name": "Hired", - "url": "https://lhh.com" - }, - { - "name": "Honey", - "url": "https://joinhoney.com" - }, - { - "name": "HowStuffWorks", - "url": "https://howstuffworks.com" - }, - { - "name": "HugeIcons Pro", - "url": "https://hugeicons.com" - }, - { - "name": "Humble Bundle", - "url": "https://humblebundle.com" - }, - { - "name": "I Have No TV", - "url": "https://ihavenotv.com" - }, - { - "name": "I Miss the Office", - "url": "https://imisstheoffice.eu" - }, - { - "name": "IcoMoon", - "url": "https://icomoon.io" - }, - { - "name": "Iconfinder", - "url": "https://iconfinder.com" - }, - { - "name": "Icon Packs", - "url": "https://iconpacks.net" - }, - { - "name": "Iconshock", - "url": "https://iconshock.com" - }, - { - "name": "Iconz Design", - "url": "https://iconz.design" - }, - { - "name": "iFixit", - "url": "https://ifixit.com" - }, - { - "name": "IFTTT", - "url": "https://ifttt.com" - }, - { - "name": "Illlustrations", - "url": "https://illlustrations.co" - }, - { - "name": "Illustration Kit", - "url": "https://illustrationkit.com" - }, - { - "name": "IMSDB", - "url": "https://imsdb.com" - }, - { - "name": "Incompetech", - "url": "https://incompetech.com" - }, - { - "name": "Incredibox", - "url": "https://incredibox.com" - }, - { - "name": "InnerBod", - "url": "https://innerbody.com" - }, - { - "name": "Instructables", - "url": "https://instructables.com" - }, - { - "name": "Integromat", - "url": "https://make.com" - }, - { - "name": "Investopedia", - "url": "https://investopedia.com" - }, - { - "name": "Japanese Wiki Corpus", - "url": "https://japanesewiki.com" - }, - { - "name": "Jitter.Video", - "url": "https://jitter.video" - }, - { - "name": "Jobspresso", - "url": "https://jobspresso.co" - }, - { - "name": "JPEG-Optimizer", - "url": "https://jpeg-optimizer.com" - }, - { - "name": "JS Remotely", - "url": "https://jsremotely.com" - }, - { - "name": "JScreenFix", - "url": "https://jscreenfix.com" - }, - { - "name": "JSON Resume", - "url": "https://jsonresume.io" - }, - { - "name": "Just Join", - "url": "https://justjoin.it" - }, - { - "name": "Just the Recipe", - "url": "https://justtherecipe.com" - }, - { - "name": "JustRemote", - "url": "https://justremote.co" - }, - { - "name": "JustWatch", - "url": "https://justwatch.com" - }, - { - "name": "Kanopy", - "url": "https://kanopy.com" - }, - { - "name": "Kassellabs", - "url": "https://kassellabs.io" - }, - { - "name": "Key Differences", - "url": "https://keydifferences.com" - }, - { - "name": "Keybase", - "url": "https://keybase.io" - }, - { - "name": "KeyValues", - "url": "https://keyvalues.com" - }, - { - "name": "KHInsider", - "url": "https://khinsider.com" - }, - { - "name": "Killed by Google", - "url": "https://killedbygoogle.com" - }, - { - "name": "Kimovil", - "url": "https://kimovil.com" - }, - { - "name": "Lalal.ai", - "url": "https://www.lalal.ai" - }, - { - "name": "Learn Anything", - "url": "https://learn-anything.xyz" - }, - { - "name": "LendingTree", - "url": "https://lendingtree.com" - }, - { - "name": "Lightyear.fm", - "url": "https://lightyear.fm" - }, - { - "name": "LittleSis", - "url": "https://littlesis.org" - }, - { - "name": "Looria", - "url": "https://looria.com" - }, - { - "name": "Lucidchart", - "url": "https://lucidchart.com" - }, - { - "name": "Lunar", - "url": "https://lunar.fyi" - }, - { - "name": "Manuals Lib", - "url": "https://manualslib.com" - }, - { - "name": "Map Crunch", - "url": "https://mapcrunch.com" - }, - { - "name": "Masterworks", - "url": "https://masterworks.com" - }, - { - "name": "MediaFire", - "url": "https://mediafire.com" - }, - { - "name": "Mixlr", - "url": "https://mixlr.com" - }, - { - "name": "Moises AI", - "url": "https://moises.ai" - }, - { - "name": "Money", - "url": "https://money.com" - }, - { - "name": "Mountain Project", - "url": "https://mountainproject.com" - }, - { - "name": "Movie Map", - "url": "https://movie-map.com" - }, - { - "name": "Movie Sounds", - "url": "https://movie-sounds.org" - }, - { - "name": "MP3Cut", - "url": "https://mp3cut.net" - }, - { - "name": "Murmel", - "url": "https://murmel.social" - }, - { - "name": "Muscle Wiki", - "url": "https://musclewiki.com" - }, - { - "name": "Music-Map", - "url": "https://music-map.com" - }, - { - "name": "MusicTheory.net", - "url": "https://musictheory.net" - }, - { - "name": "MyFonts", - "url": "https://myfonts.com" - }, - { - "name": "MyFridgeFood", - "url": "https://myfridgefood.com" - }, - { - "name": "Nameberry", - "url": "https://nameberry.com" - }, - { - "name": "Namechk", - "url": "https://namechk.com" - }, - { - "name": "Ncase", - "url": "https://ncase.me" - }, - { - "name": "News in Levels", - "url": "https://newsinlevels.com" - }, - { - "name": "Noisli", - "url": "https://noisli.com" - }, - { - "name": "Notes.io", - "url": "https://notes.io" - }, - { - "name": "Novoresume", - "url": "https://novoresume.com" - }, - { - "name": "Ocoya", - "url": "https://ocoya.com" - }, - { - "name": "Old Computers Museum", - "url": "https://oldcomputers.net" - }, - { - "name": "Online Tone Generator", - "url": "https://onlinetonegenerator.com" - }, - { - "name": "Online-Convert", - "url": "https://online-convert.com" - }, - { - "name": "OnlineConversion", - "url": "https://onlineconversion.com" - }, - { - "name": "Online OCR", - "url": "https://onlineocr.net" - }, - { - "name": "OpenWeatherMap", - "url": "https://openweathermap.org" - }, - { - "name": "OrgPad", - "url": "https://orgpad.com" - }, - { - "name": "Passport Index", - "url": "https://passportindex.org" - }, - { - "name": "PDF Candy", - "url": "https://pdfcandy.com" - }, - { - "name": "PDF2DOC", - "url": "https://pdf2doc.com" - }, - { - "name": "PDFescape", - "url": "https://pdfescape.com" - }, - { - "name": "PfpMaker", - "url": "https://pfpmaker.com" - }, - { - "name": "PIDGI Wiki ", - "url": "https://pidgi.net" - }, - { - "name": "PimEyes", - "url": "https://pimeyes.com" - }, - { - "name": "Pipl ", - "url": "https://pipl.com" - }, - { - "name": "PixelBazaar", - "url": "https://pixelbazaar.com" - }, - { - "name": "PixelPaper", - "url": "https://pixelpaper.io" - }, - { - "name": "Ponly", - "url": "https://ponly.com" - }, - { - "name": "PowerToFly", - "url": "https://powertofly.com" - }, - { - "name": "Pretzel Rocks", - "url": "https://pretzel.rocks" - }, - { - "name": "PrintIt", - "url": "https://printit.work" - }, - { - "name": "Prismatext", - "url": "https://prismatext.com" - }, - { - "name": "Puffin Maps", - "url": "https://puffinmaps.com" - }, - { - "name": "Puzzle Loop ", - "url": "https://puzzle-loop.com" - }, - { - "name": "QuoteMaster", - "url": "https://quotemaster.org" - }, - { - "name": "Radio Garden", - "url": "https://radio.garden" - }, - { - "name": "Radiooooo", - "url": "https://radiooooo.com" - }, - { - "name": "Radiosondy", - "url": "https://radiosondy.info" - }, - { - "name": "Rainy Mood", - "url": "https://rainymood.com" - }, - { - "name": "Random Street View", - "url": "https://randomstreetview.com" - }, - { - "name": "Rap4Ever", - "url": "https://rap4all.com" - }, - { - "name": "RareFilm", - "url": "https://rarefilm.net" - }, - { - "name": "Rattibha", - "url": "https://rattibha.com" - }, - { - "name": "Reddit List ", - "url": "https://redditlist.com" - }, - { - "name": "RedditSearch.io", - "url": "https://redditsearch.io" - }, - { - "name": "Reelgood", - "url": "https://reelgood.com" - }, - { - "name": "Reface", - "url": "https://reface.ai" - }, - { - "name": "Rejected.us", - "url": "https://rejected.us" - }, - { - "name": "Relanote", - "url": "https://relanote.com" - }, - { - "name": "Remote Leaf", - "url": "https://remoteleaf.com" - }, - { - "name": "Remote OK", - "url": "https://remoteok.com" - }, - { - "name": "Remote Starter Kit ", - "url": "https://remotestarterkit.com" - }, - { - "name": "Remote.co", - "url": "https://remote.co" - }, - { - "name": "Remote Base ", - "url": "https://remotebase.com" - }, - { - "name": "Remote Bear", - "url": "https://remotebear.io" - }, - { - "name": "Remove.bg", - "url": "https://remove.bg" - }, - { - "name": "Respresso", - "url": "https://respresso.io" - }, - { - "name": "Reveddit", - "url": "https://reveddit.com" - }, - { - "name": "Rhymer", - "url": "https://rhymer.com" - }, - { - "name": "RhymeZone", - "url": "https://rhymezone.com" - }, - { - "name": "Ribbet", - "url": "https://ribbet.com" - }, - { - "name": "Roadmap.sh", - "url": "https://roadmap.sh" - }, - { - "name": "Roadtrippers", - "url": "https://roadtrippers.com" - }, - { - "name": "RxResu.me", - "url": "https://rxresu.me" - }, - { - "name": "SchemeColor", - "url": "https://schemecolor.com" - }, - { - "name": "Screenshot.Guru", - "url": "https://screenshot.guru" - }, - { - "name": "SeatGuru", - "url": "https://seatguru.com" - }, - { - "name": "Sessions", - "url": "https://sessions.us" - }, - { - "name": "Shottr", - "url": "https://shottr.cc" - }, - { - "name": "Signature Maker", - "url": "https://signature-maker.net" - }, - { - "name": "Skip The Drive", - "url": "https://skipthedrive.com" - }, - { - "name": "Slowby", - "url": "https://slowby.travel" - }, - { - "name": "Small World", - "url": "https://smallworld.kiwi" - }, - { - "name": "SmallPDF", - "url": "https://smallpdf.com" - }, - { - "name": "Social Image Maker", - "url": "https://socialimagemaker.io" - }, - { - "name": "Social Sizes", - "url": "https://socialsizes.io" - }, - { - "name": "SoundLove", - "url": "https://soundlove.se" - }, - { - "name": "Spline", - "url": "https://spline.design" - }, - { - "name": "Starkey Comics", - "url": "https://starkeycomics.com" - }, - - { - "name": "Statista", - "url": "https://statista.com" - }, - { - "name": "Stolen Camera Finder", - "url": "https://stolencamerafinder.com" - }, - { - "name": "Strobe.Cool", - "url": "https://strobe.cool" - }, - { - "name": "Sumo", - "url": "https://sumo.app" - }, - { - "name": "SuperMeme AI", - "url": "https://supermeme.ai" - }, - { - "name": "Synthesia", - "url": "https://synthesia.io" - }, - { - "name": "TablerIcons", - "url": "https://tablericons.com" - }, - { - "name": "Tango", - "url": "https://tango.us" - }, - { - "name": "TasteDive", - "url": "https://tastedive.com" - }, - { - "name": "TechSpecs", - "url": "https://techspecs.io" - }, - { - "name": "Teoria", - "url": "https://teoria.com" - }, - { - "name": "Text Faces", - "url": "https://textfac.es" - }, - { - "name": "The Balance Money", - "url": "https://thebalancemoney.com" - }, - { - "name": "The Punctuation Guide", - "url": "https://thepunctuationguide.com" - }, - { - "name": "This to That", - "url": "https://thistothat.com" - }, - { - "name": "This vs That", - "url": "https://thisvsthat.io" - }, - { - "name": "ThreadReaderApp ", - "url": "https://threadreaderapp.com" - }, - { - "name": "Thumbly", - "url": "https://tokee.ai" - }, - { - "name": "Tiii.me", - "url": "https://tiii.me" - }, - { - "name": "TikTok Video Downloader", - "url": "https://ttvdl.com" - }, - { - "name": "Time and Date", - "url": "https://timeanddate.com" - }, - { - "name": "Time.is", - "url": "https://time.is" - }, - { - "name": "Title Case", - "url": "https://titlecase.com" - }, - { - "name": "Toaster Central", - "url": "https://toastercentral.com" - }, - { - "name": "Tongue-Twister ", - "url": "https://tongue-twister.net" - }, - { - "name": "TradingView", - "url": "https://tradingview.com" - }, - { - "name": "Transparent Textures", - "url": "https://transparenttextures.com" - }, - { - "name": "Tubi TV", - "url": "https://tubitv.com" - }, - { - "name": "Tunefind", - "url": "https://tunefind.com" - }, - { - "name": "TuneMyMusic", - "url": "https://tunemymusic.com" - }, - { - "name": "Tweepsmap", - "url": "https://fedica.com" - }, - { - "name": "Two Peas and Their Pod", - "url": "https://twopeasandtheirpod.com" - }, - { - "name": "Typatone", - "url": "https://typatone.com" - }, - { - "name": "Under Glass", - "url": "https://underglass.io" - }, - { - "name": "UniCorner", - "url": "https://unicorner.news" - }, - { - "name": "Unita", - "url": "https://unita.co" - }, - { - "name": "UnitConverters", - "url": "https://unitconverters.net" - }, - { - "name": "Unreadit", - "url": "https://unreadit.com" - }, - { - "name": "Unscreen", - "url": "https://unscreen.com" - }, - { - "name": "UnTools ", - "url": "https://untools.co" - }, - { - "name": "Upwork", - "url": "https://upwork.com" - }, - { - "name": "UTF8 Icons", - "url": "https://utf8icons.com" - }, - { - "name": "Vector Magic", - "url": "https://vectormagic.com" - }, - { - "name": "Virtual Vacation", - "url": "https://virtualvacation.us" - }, - { - "name": "Virtual Vocations", - "url": "https://virtualvocations.com" - }, - { - "name": "Visiwig", - "url": "https://visiwig.com" - }, - { - "name": "Visual CV", - "url": "https://visualcv.com" - }, - { - "name": "Vocus.io", - "url": "https://vocus.io" - }, - { - "name": "Voscreen", - "url": "https://voscreen.com" - }, - { - "name": "Wanderprep", - "url": "https://wanderprep.com" - }, - { - "name": "Warmshowers", - "url": "https:/warmshowers.org" - }, - { - "name": "Watch Documentaries", - "url": "https://watchdocumentaries.com" - }, - { - "name": "We Work Remotely", - "url": "https://weworkremotely.com" - }, - { - "name": "Web2PDFConvert", - "url": "https://web2pdfconvert.com" - }, - { - "name": "Welcome to My Garden", - "url": "https://welcometomygarden.org" - }, - { - "name": "When2meet ", - "url": "https://when2meet.com" - }, - { - "name": "Where's George", - "url": "https://wheresgeorge.com" - }, - { - "name": "Where's Willy", - "url": "https://whereswilly.com" - }, - { - "name": "WikiHow", - "url": "https://wikihow.com" - }, - { - "name": "Windy", - "url": "https://www.windy.com" - }, - { - "name": "WonderHowTo", - "url": "https://wonderhowto.com" - }, - { - "name": "Working Nomads", - "url": "https://workingnomads.com" - }, - { - "name": "Wormhole", - "url": "https://wormhole.app" - }, - { - "name": "Y Combinator Jobs", - "url": "https://ycombinator.com" - }, - { - "name": "Yes Promo", - "url": "https://yespromo.me" - }, - { - "name": "YouGlish", - "url": "https://youglish.com" - }, - { - "name": "Zamzar", - "url": "https://zamzar.com" - }, - { - "name": "Zippyshare", - "url": "https://zippyshare.com" - }, - { - "name": "Zoom Earth", - "url": "https://zoom.earth" - }, - { - "name": "Zoom.it", - "url": "https://zoom.it" + "name": "Apple", + "url": "https://www.apple.com" } ] diff --git a/Server/utils/demoMonitorsOld.json b/Server/utils/demoMonitorsOld.json new file mode 100644 index 000000000..ffce7d226 --- /dev/null +++ b/Server/utils/demoMonitorsOld.json @@ -0,0 +1,1271 @@ +[ + { + "name": "0to255", + "url": "https://0to255.com" + }, + { + "name": "10015.io", + "url": "https://10015.io" + }, + { + "name": "3DIcons", + "url": "https://3dicons.co" + }, + { + "name": "About.me", + "url": "https://about.me" + }, + { + "name": "Alias", + "url": "https://alias.co" + }, + { + "name": "All About Berlin", + "url": "https://allaboutberlin.com" + }, + { + "name": "All Acronyms", + "url": "https://allacronyms.com" + }, + { + "name": "All You Can Read ", + "url": "https://allyoucanread.com" + }, + { + "name": "AllTrails", + "url": "https://alltrails.com" + }, + { + "name": "Anotepad", + "url": "https://anotepad.com" + }, + { + "name": "AnswerSocrates", + "url": "https://answersocrates.com" + }, + { + "name": "AnswerThePublic ", + "url": "https://answerthepublic.com" + }, + { + "name": "Apollo ", + "url": "https://apollo.io" + }, + { + "name": "ArrayList", + "url": "https://arraylist.org" + }, + { + "name": "Ask Difference", + "url": "https://askdifference.com" + }, + { + "name": "Audd.io", + "url": "https://audd.io" + }, + { + "name": "Audiocheck", + "url": "https://audiocheck.net" + }, + { + "name": "Audionautix", + "url": "https://audionautix.com" + }, + { + "name": "Authentic Jobs", + "url": "https://authenticjobs.com" + }, + { + "name": "Behind the Name", + "url": "https://behindthename.com" + }, + { + "name": "Bilim Terimleri", + "url": "https://terimler.org" + }, + { + "name": "BitBof", + "url": "https://bitbof.com" + }, + { + "name": "Blank Page", + "url": "https://blank.page" + }, + { + "name": "Bonanza", + "url": "https://bonanza.com" + }, + { + "name": "BookCrossing", + "url": "https://bookcrossing.com" + }, + { + "name": "Browse AI", + "url": "https://browse.ai" + }, + { + "name": "Bubbl.us", + "url": "https://bubbl.us" + }, + { + "name": "Business Model Toolbox", + "url": "https://bmtoolbox.net" + }, + { + "name": "ByClickDownloader", + "url": "https://byclickdownloader.com" + }, + { + "name": "Calligraphr", + "url": "https://calligraphr.com" + }, + { + "name": "CertificateClaim", + "url": "https://certificateclaim.com" + }, + { + "name": "Chosic", + "url": "https://chosic.com" + }, + { + "name": "ClipDrop", + "url": "https://clipdrop.co" + }, + { + "name": "CloudConvert", + "url": "https://cloudconvert.com" + }, + { + "name": "CodingFont", + "url": "https://codingfont.com" + }, + { + "name": "Color Hunt", + "url": "https://colorhunt.co" + }, + { + "name": "ColorHexa", + "url": "https://colorhexa.com" + }, + { + "name": "Conversion-Tool", + "url": "https://conversion-tool.com" + }, + { + "name": "Cool Startup Jobs", + "url": "https://coolstartupjobs.com" + }, + { + "name": "Coroflot", + "url": "https://coroflot.com" + }, + { + "name": "Corrupt-a-File", + "url": "https://corrupt-a-file.net" + }, + { + "name": "Couchsurfing", + "url": "https://couchsurfing.com" + }, + { + "name": "Countries Been", + "url": "https://countriesbeen.com" + }, + { + "name": "Country Code", + "url": "https://countrycode.org" + }, + { + "name": "Creately", + "url": "https://creately.com" + }, + { + "name": "Creately ", + "url": "https://creately.com" + }, + { + "name": "Crossfade.io", + "url": "https://crossfade.io" + }, + { + "name": "Crunchbase", + "url": "https://crunchbase.com" + }, + { + "name": "CVmkr", + "url": "https://cvwizard.com" + }, + { + "name": "Daily Remote", + "url": "https://dailyremote.com" + }, + { + "name": "David Li", + "url": "https://david.li" + }, + { + "name": "DemandHunt", + "url": "https://demandhunt.com" + }, + { + "name": "Designify", + "url": "https://designify.com" + }, + { + "name": "Diff Checker", + "url": "https://diffchecker.com" + }, + { + "name": "DifferenceBetween.info", + "url": "https://differencebetween.info" + }, + { + "name": "Digital Glossary", + "url": "https://digital-glossary.com" + }, + { + "name": "Dimensions", + "url": "https://dimensions.com" + }, + { + "name": "Discoverify Music", + "url": "https://discoverifymusic.com" + }, + { + "name": "discu.eu", + "url": "https://discu.eu" + }, + { + "name": "Do It Yourself", + "url": "https://doityourself.com" + }, + { + "name": "draw.io", + "url": "https://drawio.com" + }, + { + "name": "Drumeo", + "url": "https://drumeo.com" + }, + { + "name": "Dummies", + "url": "https://dummies.com" + }, + { + "name": "Easel.ly", + "url": "https://easel.ly" + }, + { + "name": "Educalingo", + "url": "https://educalingo.com" + }, + { + "name": "Emoji Combos", + "url": "https://emojicombos.com" + }, + { + "name": "EquityBee", + "url": "https://equitybee.com" + }, + { + "name": "EquityZen", + "url": "https://equityzen.com" + }, + { + "name": "Escape Room Tips", + "url": "https://escaperoomtips.com" + }, + { + "name": "Every Noise", + "url": "https://everynoise.com" + }, + { + "name": "Every Time Zone", + "url": "https://everytimezone.com" + }, + { + "name": "Excalideck", + "url": "https://excalideck.com" + }, + { + "name": "Excalidraw", + "url": "https://excalidraw.com" + }, + { + "name": "Extract pics", + "url": "https://extract.pics" + }, + { + "name": "EZGIF", + "url": "https://ezgif.com" + }, + { + "name": "FactSlides", + "url": "https://factslides.com" + }, + { + "name": "FIGR ", + "url": "https://figr.app" + }, + { + "name": "Fine Dictionary", + "url": "https://finedictionary.com" + }, + { + "name": "Fiverr", + "url": "https://fiverr.com" + }, + { + "name": "Fix It Club", + "url": "https://fixitclub.com" + }, + { + "name": "Flightradar24", + "url": "https://flightradar24.com" + }, + { + "name": "FlowCV ", + "url": "https://flowcv.com" + }, + { + "name": "Font Squirrel", + "url": "https://fontsquirrel.com" + }, + { + "name": "FontAwesome", + "url": "https://fontawesome.com" + }, + { + "name": "Fontello ", + "url": "https://fontello.com" + }, + { + "name": "Form to Chatbot", + "url": "https://formtochatbot.com" + }, + { + "name": "Founder Resources", + "url": "https://founderresources.io" + }, + { + "name": "Franz", + "url": "https://meetfranz.com" + }, + { + "name": "Fraze It", + "url": "https://fraze.it" + }, + { + "name": "Freecycle", + "url": "https://freecycle.org" + }, + { + "name": "FreeType", + "url": "https://freetype.org" + }, + { + "name": "FutureM", + "url": "https://futureme.org" + }, + { + "name": "Generated.Photos", + "url": "https://generated.photos" + }, + { + "name": "Get Human", + "url": "https://gethuman.com" + }, + { + "name": "Go Bento", + "url": "https://gobento.com" + }, + { + "name": "Good CV", + "url": "https://goodcv.com" + }, + { + "name": "Grammar Monster", + "url": "https://grammar-monster.com" + }, + { + "name": "Grammar Book", + "url": "https://grammarbook.com" + }, + { + "name": "Gummy Search", + "url": "https://gummysearch.com" + }, + { + "name": "Gumroad", + "url": "https://gumroad.com" + }, + { + "name": "HealthIcons", + "url": "https://healthicons.org" + }, + { + "name": "HexColor", + "url": "https://hexcolor.co" + }, + { + "name": "Hidden Life Radio", + "url": "https://hiddenliferadio.com" + }, + { + "name": "Hired", + "url": "https://lhh.com" + }, + { + "name": "Honey", + "url": "https://joinhoney.com" + }, + { + "name": "HowStuffWorks", + "url": "https://howstuffworks.com" + }, + { + "name": "HugeIcons Pro", + "url": "https://hugeicons.com" + }, + { + "name": "Humble Bundle", + "url": "https://humblebundle.com" + }, + { + "name": "I Have No TV", + "url": "https://ihavenotv.com" + }, + { + "name": "I Miss the Office", + "url": "https://imisstheoffice.eu" + }, + { + "name": "IcoMoon", + "url": "https://icomoon.io" + }, + { + "name": "Iconfinder", + "url": "https://iconfinder.com" + }, + { + "name": "Icon Packs", + "url": "https://iconpacks.net" + }, + { + "name": "Iconshock", + "url": "https://iconshock.com" + }, + { + "name": "Iconz Design", + "url": "https://iconz.design" + }, + { + "name": "iFixit", + "url": "https://ifixit.com" + }, + { + "name": "IFTTT", + "url": "https://ifttt.com" + }, + { + "name": "Illlustrations", + "url": "https://illlustrations.co" + }, + { + "name": "Illustration Kit", + "url": "https://illustrationkit.com" + }, + { + "name": "IMSDB", + "url": "https://imsdb.com" + }, + { + "name": "Incompetech", + "url": "https://incompetech.com" + }, + { + "name": "Incredibox", + "url": "https://incredibox.com" + }, + { + "name": "InnerBod", + "url": "https://innerbody.com" + }, + { + "name": "Instructables", + "url": "https://instructables.com" + }, + { + "name": "Integromat", + "url": "https://make.com" + }, + { + "name": "Investopedia", + "url": "https://investopedia.com" + }, + { + "name": "Japanese Wiki Corpus", + "url": "https://japanesewiki.com" + }, + { + "name": "Jitter.Video", + "url": "https://jitter.video" + }, + { + "name": "Jobspresso", + "url": "https://jobspresso.co" + }, + { + "name": "JPEG-Optimizer", + "url": "https://jpeg-optimizer.com" + }, + { + "name": "JS Remotely", + "url": "https://jsremotely.com" + }, + { + "name": "JScreenFix", + "url": "https://jscreenfix.com" + }, + { + "name": "JSON Resume", + "url": "https://jsonresume.io" + }, + { + "name": "Just Join", + "url": "https://justjoin.it" + }, + { + "name": "Just the Recipe", + "url": "https://justtherecipe.com" + }, + { + "name": "JustRemote", + "url": "https://justremote.co" + }, + { + "name": "JustWatch", + "url": "https://justwatch.com" + }, + { + "name": "Kanopy", + "url": "https://kanopy.com" + }, + { + "name": "Kassellabs", + "url": "https://kassellabs.io" + }, + { + "name": "Key Differences", + "url": "https://keydifferences.com" + }, + { + "name": "Keybase", + "url": "https://keybase.io" + }, + { + "name": "KeyValues", + "url": "https://keyvalues.com" + }, + { + "name": "KHInsider", + "url": "https://khinsider.com" + }, + { + "name": "Killed by Google", + "url": "https://killedbygoogle.com" + }, + { + "name": "Kimovil", + "url": "https://kimovil.com" + }, + { + "name": "Lalal.ai", + "url": "https://www.lalal.ai" + }, + { + "name": "Learn Anything", + "url": "https://learn-anything.xyz" + }, + { + "name": "LendingTree", + "url": "https://lendingtree.com" + }, + { + "name": "Lightyear.fm", + "url": "https://lightyear.fm" + }, + { + "name": "LittleSis", + "url": "https://littlesis.org" + }, + { + "name": "Looria", + "url": "https://looria.com" + }, + { + "name": "Lucidchart", + "url": "https://lucidchart.com" + }, + { + "name": "Lunar", + "url": "https://lunar.fyi" + }, + { + "name": "Manuals Lib", + "url": "https://manualslib.com" + }, + { + "name": "Map Crunch", + "url": "https://mapcrunch.com" + }, + { + "name": "Masterworks", + "url": "https://masterworks.com" + }, + { + "name": "MediaFire", + "url": "https://mediafire.com" + }, + { + "name": "Mixlr", + "url": "https://mixlr.com" + }, + { + "name": "Moises AI", + "url": "https://moises.ai" + }, + { + "name": "Money", + "url": "https://money.com" + }, + { + "name": "Mountain Project", + "url": "https://mountainproject.com" + }, + { + "name": "Movie Map", + "url": "https://movie-map.com" + }, + { + "name": "Movie Sounds", + "url": "https://movie-sounds.org" + }, + { + "name": "MP3Cut", + "url": "https://mp3cut.net" + }, + { + "name": "Murmel", + "url": "https://murmel.social" + }, + { + "name": "Muscle Wiki", + "url": "https://musclewiki.com" + }, + { + "name": "Music-Map", + "url": "https://music-map.com" + }, + { + "name": "MusicTheory.net", + "url": "https://musictheory.net" + }, + { + "name": "MyFonts", + "url": "https://myfonts.com" + }, + { + "name": "MyFridgeFood", + "url": "https://myfridgefood.com" + }, + { + "name": "Nameberry", + "url": "https://nameberry.com" + }, + { + "name": "Namechk", + "url": "https://namechk.com" + }, + { + "name": "Ncase", + "url": "https://ncase.me" + }, + { + "name": "News in Levels", + "url": "https://newsinlevels.com" + }, + { + "name": "Noisli", + "url": "https://noisli.com" + }, + { + "name": "Notes.io", + "url": "https://notes.io" + }, + { + "name": "Novoresume", + "url": "https://novoresume.com" + }, + { + "name": "Ocoya", + "url": "https://ocoya.com" + }, + { + "name": "Old Computers Museum", + "url": "https://oldcomputers.net" + }, + { + "name": "Online Tone Generator", + "url": "https://onlinetonegenerator.com" + }, + { + "name": "Online-Convert", + "url": "https://online-convert.com" + }, + { + "name": "OnlineConversion", + "url": "https://onlineconversion.com" + }, + { + "name": "Online OCR", + "url": "https://onlineocr.net" + }, + { + "name": "OpenWeatherMap", + "url": "https://openweathermap.org" + }, + { + "name": "OrgPad", + "url": "https://orgpad.com" + }, + { + "name": "Passport Index", + "url": "https://passportindex.org" + }, + { + "name": "PDF Candy", + "url": "https://pdfcandy.com" + }, + { + "name": "PDF2DOC", + "url": "https://pdf2doc.com" + }, + { + "name": "PDFescape", + "url": "https://pdfescape.com" + }, + { + "name": "PfpMaker", + "url": "https://pfpmaker.com" + }, + { + "name": "PIDGI Wiki ", + "url": "https://pidgi.net" + }, + { + "name": "PimEyes", + "url": "https://pimeyes.com" + }, + { + "name": "Pipl ", + "url": "https://pipl.com" + }, + { + "name": "PixelBazaar", + "url": "https://pixelbazaar.com" + }, + { + "name": "PixelPaper", + "url": "https://pixelpaper.io" + }, + { + "name": "Ponly", + "url": "https://ponly.com" + }, + { + "name": "PowerToFly", + "url": "https://powertofly.com" + }, + { + "name": "Pretzel Rocks", + "url": "https://pretzel.rocks" + }, + { + "name": "PrintIt", + "url": "https://printit.work" + }, + { + "name": "Prismatext", + "url": "https://prismatext.com" + }, + { + "name": "Puffin Maps", + "url": "https://puffinmaps.com" + }, + { + "name": "Puzzle Loop ", + "url": "https://puzzle-loop.com" + }, + { + "name": "QuoteMaster", + "url": "https://quotemaster.org" + }, + { + "name": "Radio Garden", + "url": "https://radio.garden" + }, + { + "name": "Radiooooo", + "url": "https://radiooooo.com" + }, + { + "name": "Radiosondy", + "url": "https://radiosondy.info" + }, + { + "name": "Rainy Mood", + "url": "https://rainymood.com" + }, + { + "name": "Random Street View", + "url": "https://randomstreetview.com" + }, + { + "name": "Rap4Ever", + "url": "https://rap4all.com" + }, + { + "name": "RareFilm", + "url": "https://rarefilm.net" + }, + { + "name": "Rattibha", + "url": "https://rattibha.com" + }, + { + "name": "Reddit List ", + "url": "https://redditlist.com" + }, + { + "name": "RedditSearch.io", + "url": "https://redditsearch.io" + }, + { + "name": "Reelgood", + "url": "https://reelgood.com" + }, + { + "name": "Reface", + "url": "https://reface.ai" + }, + { + "name": "Rejected.us", + "url": "https://rejected.us" + }, + { + "name": "Relanote", + "url": "https://relanote.com" + }, + { + "name": "Remote Leaf", + "url": "https://remoteleaf.com" + }, + { + "name": "Remote OK", + "url": "https://remoteok.com" + }, + { + "name": "Remote Starter Kit ", + "url": "https://remotestarterkit.com" + }, + { + "name": "Remote.co", + "url": "https://remote.co" + }, + { + "name": "Remote Base ", + "url": "https://remotebase.com" + }, + { + "name": "Remote Bear", + "url": "https://remotebear.io" + }, + { + "name": "Remove.bg", + "url": "https://remove.bg" + }, + { + "name": "Respresso", + "url": "https://respresso.io" + }, + { + "name": "Reveddit", + "url": "https://reveddit.com" + }, + { + "name": "Rhymer", + "url": "https://rhymer.com" + }, + { + "name": "RhymeZone", + "url": "https://rhymezone.com" + }, + { + "name": "Ribbet", + "url": "https://ribbet.com" + }, + { + "name": "Roadmap.sh", + "url": "https://roadmap.sh" + }, + { + "name": "Roadtrippers", + "url": "https://roadtrippers.com" + }, + { + "name": "RxResu.me", + "url": "https://rxresu.me" + }, + { + "name": "SchemeColor", + "url": "https://schemecolor.com" + }, + { + "name": "Screenshot.Guru", + "url": "https://screenshot.guru" + }, + { + "name": "SeatGuru", + "url": "https://seatguru.com" + }, + { + "name": "Sessions", + "url": "https://sessions.us" + }, + { + "name": "Shottr", + "url": "https://shottr.cc" + }, + { + "name": "Signature Maker", + "url": "https://signature-maker.net" + }, + { + "name": "Skip The Drive", + "url": "https://skipthedrive.com" + }, + { + "name": "Slowby", + "url": "https://slowby.travel" + }, + { + "name": "Small World", + "url": "https://smallworld.kiwi" + }, + { + "name": "SmallPDF", + "url": "https://smallpdf.com" + }, + { + "name": "Social Image Maker", + "url": "https://socialimagemaker.io" + }, + { + "name": "Social Sizes", + "url": "https://socialsizes.io" + }, + { + "name": "SoundLove", + "url": "https://soundlove.se" + }, + { + "name": "Spline", + "url": "https://spline.design" + }, + { + "name": "Starkey Comics", + "url": "https://starkeycomics.com" + }, + + { + "name": "Statista", + "url": "https://statista.com" + }, + { + "name": "Stolen Camera Finder", + "url": "https://stolencamerafinder.com" + }, + { + "name": "Strobe.Cool", + "url": "https://strobe.cool" + }, + { + "name": "Sumo", + "url": "https://sumo.app" + }, + { + "name": "SuperMeme AI", + "url": "https://supermeme.ai" + }, + { + "name": "Synthesia", + "url": "https://synthesia.io" + }, + { + "name": "TablerIcons", + "url": "https://tablericons.com" + }, + { + "name": "Tango", + "url": "https://tango.us" + }, + { + "name": "TasteDive", + "url": "https://tastedive.com" + }, + { + "name": "TechSpecs", + "url": "https://techspecs.io" + }, + { + "name": "Teoria", + "url": "https://teoria.com" + }, + { + "name": "Text Faces", + "url": "https://textfac.es" + }, + { + "name": "The Balance Money", + "url": "https://thebalancemoney.com" + }, + { + "name": "The Punctuation Guide", + "url": "https://thepunctuationguide.com" + }, + { + "name": "This to That", + "url": "https://thistothat.com" + }, + { + "name": "This vs That", + "url": "https://thisvsthat.io" + }, + { + "name": "ThreadReaderApp ", + "url": "https://threadreaderapp.com" + }, + { + "name": "Thumbly", + "url": "https://tokee.ai" + }, + { + "name": "Tiii.me", + "url": "https://tiii.me" + }, + { + "name": "TikTok Video Downloader", + "url": "https://ttvdl.com" + }, + { + "name": "Time and Date", + "url": "https://timeanddate.com" + }, + { + "name": "Time.is", + "url": "https://time.is" + }, + { + "name": "Title Case", + "url": "https://titlecase.com" + }, + { + "name": "Toaster Central", + "url": "https://toastercentral.com" + }, + { + "name": "Tongue-Twister ", + "url": "https://tongue-twister.net" + }, + { + "name": "TradingView", + "url": "https://tradingview.com" + }, + { + "name": "Transparent Textures", + "url": "https://transparenttextures.com" + }, + { + "name": "Tubi TV", + "url": "https://tubitv.com" + }, + { + "name": "Tunefind", + "url": "https://tunefind.com" + }, + { + "name": "TuneMyMusic", + "url": "https://tunemymusic.com" + }, + { + "name": "Tweepsmap", + "url": "https://fedica.com" + }, + { + "name": "Two Peas and Their Pod", + "url": "https://twopeasandtheirpod.com" + }, + { + "name": "Typatone", + "url": "https://typatone.com" + }, + { + "name": "Under Glass", + "url": "https://underglass.io" + }, + { + "name": "UniCorner", + "url": "https://unicorner.news" + }, + { + "name": "Unita", + "url": "https://unita.co" + }, + { + "name": "UnitConverters", + "url": "https://unitconverters.net" + }, + { + "name": "Unreadit", + "url": "https://unreadit.com" + }, + { + "name": "Unscreen", + "url": "https://unscreen.com" + }, + { + "name": "UnTools ", + "url": "https://untools.co" + }, + { + "name": "Upwork", + "url": "https://upwork.com" + }, + { + "name": "UTF8 Icons", + "url": "https://utf8icons.com" + }, + { + "name": "Vector Magic", + "url": "https://vectormagic.com" + }, + { + "name": "Virtual Vacation", + "url": "https://virtualvacation.us" + }, + { + "name": "Virtual Vocations", + "url": "https://virtualvocations.com" + }, + { + "name": "Visiwig", + "url": "https://visiwig.com" + }, + { + "name": "Visual CV", + "url": "https://visualcv.com" + }, + { + "name": "Vocus.io", + "url": "https://vocus.io" + }, + { + "name": "Voscreen", + "url": "https://voscreen.com" + }, + { + "name": "Wanderprep", + "url": "https://wanderprep.com" + }, + { + "name": "Warmshowers", + "url": "https:/warmshowers.org" + }, + { + "name": "Watch Documentaries", + "url": "https://watchdocumentaries.com" + }, + { + "name": "We Work Remotely", + "url": "https://weworkremotely.com" + }, + { + "name": "Web2PDFConvert", + "url": "https://web2pdfconvert.com" + }, + { + "name": "Welcome to My Garden", + "url": "https://welcometomygarden.org" + }, + { + "name": "When2meet ", + "url": "https://when2meet.com" + }, + { + "name": "Where's George", + "url": "https://wheresgeorge.com" + }, + { + "name": "Where's Willy", + "url": "https://whereswilly.com" + }, + { + "name": "WikiHow", + "url": "https://wikihow.com" + }, + { + "name": "Windy", + "url": "https://www.windy.com" + }, + { + "name": "WonderHowTo", + "url": "https://wonderhowto.com" + }, + { + "name": "Working Nomads", + "url": "https://workingnomads.com" + }, + { + "name": "Wormhole", + "url": "https://wormhole.app" + }, + { + "name": "Y Combinator Jobs", + "url": "https://ycombinator.com" + }, + { + "name": "Yes Promo", + "url": "https://yespromo.me" + }, + { + "name": "YouGlish", + "url": "https://youglish.com" + }, + { + "name": "Zamzar", + "url": "https://zamzar.com" + }, + { + "name": "Zippyshare", + "url": "https://zippyshare.com" + }, + { + "name": "Zoom Earth", + "url": "https://zoom.earth" + }, + { + "name": "Zoom.it", + "url": "https://zoom.it" + } +] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..f71a78c1f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "bluewave-uptime", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 35fa32796e8938f4b27d38add8a4150c9c6e9d99 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 23 Jan 2025 11:41:06 -0800 Subject: [PATCH 25/76] remove local IP address exclusion from moniotr validation regex --- Client/src/Validation/validation.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 57d926f6e..e40a9c174 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -103,11 +103,6 @@ const monitorValidation = joi.object({ "(?:(?:https?|ftp):\\/\\/)?" + // user:pass BasicAuth (optional) "(?:" + - // IP address exclusion - // private & local networks - "(?!(?:10|127)(?:\\.\\d{1,3}){3})" + - "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" + - "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" + // IP address dotted notation octets // excludes loopback network 0.0.0.0 // excludes reserved space >= 224.0.0.0 From 6bb8461435a55468b21a1d6c635a4c9f8d3f4d92 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 23 Jan 2025 13:35:01 -0800 Subject: [PATCH 26/76] add port type monitors to queue mapping --- Server/service/jobQueue.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 5a9ab335c..419785540 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -5,6 +5,7 @@ const QUEUE_LOOKUP = { hardware: "hardware", http: "uptime", ping: "uptime", + port: "uptime", docker: "uptime", pagespeed: "pagespeed", }; From b72637476b81bc4e2903a2e2d7e843ac9c0a4cea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 07:21:15 -0800 Subject: [PATCH 27/76] fix cpu frequency parsing --- Client/src/Pages/Infrastructure/index.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 0bf367b99..07e02940d 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -149,8 +149,9 @@ function Infrastructure() { ]; const monitorsAsRows = monitors.map((monitor) => { + console.log(monitor); const processor = - ((monitor.checks[0]?.cpu?.usage_frequency ?? 0) / 1000).toFixed(2) + " GHz"; + ((monitor.checks[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz"; const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100; const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100; const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100; @@ -181,6 +182,7 @@ function Infrastructure() { }); let isActuallyLoading = isLoading && monitors?.length === 0; + console.log(monitorsAsRows); return ( Date: Fri, 24 Jan 2025 07:46:56 -0800 Subject: [PATCH 28/76] Update Client/src/Pages/Infrastructure/index.jsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mert Şişmanoğlu --- Client/src/Pages/Infrastructure/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 07e02940d..190478f36 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -149,7 +149,6 @@ function Infrastructure() { ]; const monitorsAsRows = monitors.map((monitor) => { - console.log(monitor); const processor = ((monitor.checks[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz"; const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100; From e693e10d7cb3012aaa019b72846231f68664f00a Mon Sep 17 00:00:00 2001 From: Alexander Holliday Date: Fri, 24 Jan 2025 07:47:24 -0800 Subject: [PATCH 29/76] Update Client/src/Pages/Infrastructure/index.jsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mert Şişmanoğlu --- Client/src/Pages/Infrastructure/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 190478f36..fbfae6a3c 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -181,7 +181,6 @@ function Infrastructure() { }); let isActuallyLoading = isLoading && monitors?.length === 0; - console.log(monitorsAsRows); return ( Date: Fri, 24 Jan 2025 17:03:38 +0000 Subject: [PATCH 30/76] fix(deps): update material-ui monorepo to v7.24.1 --- Client/package-lock.json | 34 +++++++++++++++++----------------- Client/package.json | 4 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index f58aa9d46..abbc57323 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -15,8 +15,8 @@ "@mui/lab": "6.0.0-beta.24", "@mui/material": "6.4.1", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.24.0", - "@mui/x-date-pickers": "7.24.0", + "@mui/x-data-grid": "7.24.1", + "@mui/x-date-pickers": "7.24.1", "@reduxjs/toolkit": "2.5.0", "axios": "^1.7.4", "dayjs": "1.11.13", @@ -1390,15 +1390,15 @@ "license": "MIT" }, "node_modules/@mui/x-charts": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.24.0.tgz", - "integrity": "sha512-X/zN0GxPHJC24QckFrtEbIK0HYoGwZgpNC5CV9cvRGvHphDFuNZk9xg9fJwIg1i3PPZHXRO3E+L/ee6fJ9ufhw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.24.1.tgz", + "integrity": "sha512-OdTS/nXaANPe4AoUFIDD4LlID8kK/00q+uqVOCkVClEvFQeAkj3pBaghdS4hY7rVqsCgsm+yOStQVJa9G2MR+Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", "@mui/x-charts-vendor": "7.20.0", - "@mui/x-internals": "7.24.0", + "@mui/x-internals": "7.24.1", "@react-spring/rafz": "^9.7.5", "@react-spring/web": "^9.7.5", "clsx": "^2.1.1", @@ -1448,14 +1448,14 @@ } }, "node_modules/@mui/x-data-grid": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.24.0.tgz", - "integrity": "sha512-goYTKDp+e+dXw7E+WndWUhWXTjX3aTqN8W2dCKhXnmE9Gu8dFwG6Azl7GK9l2m5YHGuqYmpWqcSG9etLdwYaVg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.24.1.tgz", + "integrity": "sha512-4sYTbMwsDotuTd2Cwa2JGTPXPWQs8RGJvocAKnIsNOzNdZNMrikE//HO35snriK8s4dauAApY7RVbeisjpVT+A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", - "@mui/x-internals": "7.24.0", + "@mui/x-internals": "7.24.1", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.1.1" @@ -1485,14 +1485,14 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.24.0.tgz", - "integrity": "sha512-oBM9Yp2H3tJ7qoHB4APQJYxZG4rz6JD4CwLzbzD9o3r+E1HGpGSLhwK3rDEz9VEjbOq8893Z2TGYLLWoyjeFXQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.24.1.tgz", + "integrity": "sha512-ykQugMQHuQKBk3kViW/r0PpubtHQOlrd54bgbdafgTMCeM2VpXvv4zimzOu5IGnM6wEN8hupC7EXZbkrT6x46w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", - "@mui/x-internals": "7.24.0", + "@mui/x-internals": "7.24.1", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1551,9 +1551,9 @@ } }, "node_modules/@mui/x-internals": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.0.tgz", - "integrity": "sha512-lYa/XLltxNMY8YAFDopIHrXda2EAoqMCilyGMuPMz+WTG+b+StlUKqtj8cgFPQ/sa5dQ2fR7R3KJdjLREKUrlQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.1.tgz", + "integrity": "sha512-9BvJzpLJnS9BDphvkiv6v0QOLxbnu8jhwcexFjtCQ2ZyxtVuVsWzGZ2npT9sGOil7+eaFDmWnJtea/tgrPvSwQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", diff --git a/Client/package.json b/Client/package.json index 247e1fe72..006bdd081 100644 --- a/Client/package.json +++ b/Client/package.json @@ -18,8 +18,8 @@ "@mui/lab": "6.0.0-beta.24", "@mui/material": "6.4.1", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.24.0", - "@mui/x-date-pickers": "7.24.0", + "@mui/x-data-grid": "7.24.1", + "@mui/x-date-pickers": "7.24.1", "@reduxjs/toolkit": "2.5.0", "axios": "^1.7.4", "dayjs": "1.11.13", From 2caa4d4609247135ec06995d0d4ec75856a0fc50 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 24 Jan 2025 19:58:41 +0000 Subject: [PATCH 31/76] chore(deps): update eslint monorepo to v9.19.0 --- Server/package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 67399c8a1..6b5868c8b 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -324,9 +324,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", - "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", + "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", "dev": true, "license": "MIT", "engines": { @@ -3071,9 +3071,9 @@ } }, "node_modules/eslint": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", - "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", + "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", "dev": true, "license": "MIT", "dependencies": { @@ -3082,7 +3082,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.18.0", + "@eslint/js": "9.19.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", From a011d8302055161dd9832e3b525ab8967adb95c0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 13:08:33 -0800 Subject: [PATCH 32/76] Create circular count component --- Client/src/Components/CircularCount/index.jsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Client/src/Components/CircularCount/index.jsx diff --git a/Client/src/Components/CircularCount/index.jsx b/Client/src/Components/CircularCount/index.jsx new file mode 100644 index 000000000..5c09ad162 --- /dev/null +++ b/Client/src/Components/CircularCount/index.jsx @@ -0,0 +1,29 @@ +import { Box } from "@mui/material"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +const CircularCount = ({ count }) => { + const theme = useTheme(); + return ( + + {count} + + ); +}; + +CircularCount.propTypes = { + count: PropTypes.number, +}; + +export default CircularCount; From a18b8172635805e7cb42901d5c069c095cdd6e07 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 13:09:05 -0800 Subject: [PATCH 33/76] refactor components out to Components dir --- .../Home/Components/ActionsMenu/index.jsx | 240 ++++++++++++++++ .../Uptime/Home/Components/Host/index.jsx | 64 +++++ .../Home/Components/LoadingSpinner/index.jsx | 46 +++ .../Home/Components/SearchComponent/index.jsx | 43 +++ .../Uptime/Home/Components/Skeleton/index.jsx | 61 ++++ .../Home/Components/StatusBoxes/index.jsx | 36 +++ .../Home/Components/StatusBoxes/skeleton.jsx | 31 +++ .../Home/Components/StatusBoxes/statusBox.jsx | 107 +++++++ .../Home/Components/UptimeDataTable/index.jsx | 263 ++++++++++++++++++ .../Components/UptimeDataTable/skeleton.jsx | 21 ++ 10 files changed, 912 insertions(+) create mode 100644 Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/Host/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx b/Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx new file mode 100644 index 000000000..f0561d6e6 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx @@ -0,0 +1,240 @@ +import { useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; +import { createToast } from "../../../../../Utils/toastUtils"; +import { logger } from "../../../../../Utils/Logger"; +import { IconButton, Menu, MenuItem } from "@mui/material"; +import { + deleteUptimeMonitor, + pauseUptimeMonitor, +} from "../../../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import Settings from "../../../../../assets/icons/settings-bold.svg?react"; +import PropTypes from "prop-types"; +import Dialog from "../../../../../Components/Dialog"; + +const ActionsMenu = ({ + monitor, + isAdmin, + updateRowCallback, + pauseCallback, + setIsLoading, +}) => { + const [anchorEl, setAnchorEl] = useState(null); + const [actions, setActions] = useState({}); + const [isOpen, setIsOpen] = useState(false); + const dispatch = useDispatch(); + const theme = useTheme(); + const authState = useSelector((state) => state.auth); + const authToken = authState.authToken; + const { isLoading } = useSelector((state) => state.uptimeMonitors); + + const handleRemove = async (event) => { + event.preventDefault(); + event.stopPropagation(); + let monitor = { _id: actions.id }; + const action = await dispatch( + deleteUptimeMonitor({ authToken: authState.authToken, monitor }) + ); + if (action.meta.requestStatus === "fulfilled") { + setIsOpen(false); // close modal + updateRowCallback(); + createToast({ body: "Monitor deleted successfully." }); + } else { + createToast({ body: "Failed to delete monitor." }); + } + }; + + const handlePause = async () => { + try { + setIsLoading(true); + const action = await dispatch( + pauseUptimeMonitor({ authToken, monitorId: monitor._id }) + ); + if (pauseUptimeMonitor.fulfilled.match(action)) { + const state = action?.payload?.data.isActive === false ? "paused" : "resumed"; + createToast({ body: `Monitor ${state} successfully.` }); + pauseCallback(); + } else { + throw new Error(action?.error?.message ?? "Failed to pause monitor."); + } + } catch (error) { + logger.error("Error pausing monitor:", monitor._id, error); + createToast({ body: "Failed to pause monitor." }); + } + }; + + const openMenu = (event, id, url) => { + event.preventDefault(); + event.stopPropagation(); + setAnchorEl(event.currentTarget); + setActions({ id: id, url: url }); + }; + + const openRemove = (e) => { + closeMenu(e); + setIsOpen(true); + }; + + const closeMenu = (e) => { + e.stopPropagation(); + setAnchorEl(null); + }; + + const navigate = useNavigate(); + return ( + <> + { + event.stopPropagation(); + openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url); + }} + sx={{ + "&:focus": { + outline: "none", + }, + "& svg path": { + stroke: theme.palette.primary.contrastTextTertiary, + }, + }} + > + + + + closeMenu(e)} + disableScrollLock + slotProps={{ + paper: { + sx: { + "& ul": { + p: theme.spacing(2.5), + backgroundColor: theme.palette.primary.main, + }, + "& li": { m: 0, color: theme.palette.primary.contrastTextSecondary }, + /* + This should not be set automatically on the last of type + "& li:last-of-type": { + color: theme.palette.error.main, + }, */ + }, + }, + }} + > + {actions.url !== null ? ( + { + closeMenu(e); + e.stopPropagation(); + window.open(actions.url, "_blank", "noreferrer"); + }} + > + Open site + + ) : ( + "" + )} + { + e.stopPropagation(); + navigate(`/uptime/${actions.id}`); + }} + > + Details + + {/* TODO - pass monitor id to Incidents page */} + { + e.stopPropagation(); + navigate(`/incidents/${actions.id}`); + }} + > + Incidents + + {isAdmin && ( + { + e.stopPropagation(); + + navigate(`/uptime/configure/${actions.id}`); + }} + > + Configure + + )} + {isAdmin && ( + { + e.stopPropagation(); + navigate(`/uptime/create/${actions.id}`); + }} + > + Clone + + )} + {isAdmin && ( + { + closeMenu(e); + + e.stopPropagation(); + handlePause(e); + }} + > + {monitor?.isActive === true ? "Pause" : "Resume"} + + )} + {isAdmin && ( + { + e.stopPropagation(); + openRemove(e); + }} + sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }} + > + Remove + + )} + + { + e.stopPropagation(); + setIsOpen(false); + }} + confirmationButtonLabel="Delete" + /* Do we need stop propagation? */ + onConfirm={(e) => { + e.stopPropagation(); + handleRemove(e); + }} + isLoading={isLoading} + modelTitle="modal-delete-monitor" + modelDescription="delete-monitor-confirmation" + /> + + ); +}; + +ActionsMenu.propTypes = { + monitor: PropTypes.shape({ + _id: PropTypes.string, + url: PropTypes.string, + type: PropTypes.string, + isActive: PropTypes.bool, + }).isRequired, + isAdmin: PropTypes.bool, + updateRowCallback: PropTypes.func, + pauseCallback: PropTypes.func, + setIsLoading: PropTypes.func, +}; + +export default ActionsMenu; diff --git a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx new file mode 100644 index 000000000..ec78875f4 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx @@ -0,0 +1,64 @@ +import { Box, Typography } from "@mui/material"; +import PropTypes from "prop-types"; +/** + * Host component. + * This subcomponent receives a params object and displays the host details. + * + * @component + * @param {Object} params - An object containing the following properties: + * @param {string} params.url - The URL of the host. + * @param {string} params.title - The name of the host. + * @param {string} params.percentageColor - The color of the percentage text. + * @param {number} params.percentage - The percentage to display. + * @returns {React.ElementType} Returns a div element with the host details. + */ +const Host = ({ url, title, percentageColor, percentage }) => { + const noTitle = title === undefined || title === url; + return ( + + + {title} + + {percentageColor && percentage && ( + + {percentage}% + + )} + {!noTitle && {url}} + + ); +}; + +Host.propTypes = { + title: PropTypes.string, + percentageColor: PropTypes.string, + percentage: PropTypes.string, + url: PropTypes.string, +}; + +export default Host; diff --git a/Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx b/Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx new file mode 100644 index 000000000..a649473af --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx @@ -0,0 +1,46 @@ +import { CircularProgress, Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +const LoadingSpinner = ({ shouldRender }) => { + const theme = useTheme(); + if (shouldRender === false) { + return; + } + + return ( + <> + + + + + + ); +}; + +LoadingSpinner.propTypes = { + shouldRender: PropTypes.bool, +}; + +export default LoadingSpinner; diff --git a/Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx b/Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx new file mode 100644 index 000000000..d93e6672b --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import Search from "../../../../../Components/Inputs/Search"; +import { Box } from "@mui/material"; +import useDebounce from "../../Hooks/useDebounce"; +import { useEffect } from "react"; +import PropTypes from "prop-types"; + +const SearchComponent = ({ monitors, onSearchChange, setIsSearching }) => { + const [localSearch, setLocalSearch] = useState(""); + const debouncedSearch = useDebounce(localSearch, 500); + useEffect(() => { + onSearchChange(debouncedSearch); + setIsSearching(false); + }, [debouncedSearch, onSearchChange, setIsSearching]); + + const handleSearch = (value) => { + setLocalSearch(value); + setIsSearching(true); + }; + + return ( + + + + ); +}; + +SearchComponent.propTypes = { + monitors: PropTypes.array, + onSearchChange: PropTypes.func, + setIsSearching: PropTypes.func, +}; + +export default SearchComponent; diff --git a/Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx new file mode 100644 index 000000000..4e55be536 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx @@ -0,0 +1,61 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +/** + * Renders a skeleton layout. + * + * @returns {JSX.Element} + */ +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..ac9eca47f --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx @@ -0,0 +1,36 @@ +import PropTypes from "prop-types"; +import { Stack } from "@mui/material"; +import StatusBox from "./statusBox"; +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton"; + +const StatusBoxes = ({ shouldRender, monitorsSummary }) => { + const theme = useTheme(); + if (!shouldRender) return ; + return ( + + + + + + ); +}; + +StatusBoxes.propTypes = { + monitorsSummary: PropTypes.object.isRequired, +}; + +export default StatusBoxes; diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx new file mode 100644 index 000000000..df6e08ccd --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx @@ -0,0 +1,31 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx new file mode 100644 index 000000000..b157857ea --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx @@ -0,0 +1,107 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Stack, Typography } from "@mui/material"; +import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react"; +import Background from "../../../../../assets/Images/background-grid.svg?react"; +import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react"; + +const StatusBox = ({ title, value }) => { + const theme = useTheme(); + + let sharedStyles = { + position: "absolute", + right: 8, + opacity: 0.5, + "& svg path": { stroke: theme.palette.primary.contrastTextTertiary }, + }; + + let color; + let icon; + if (title === "up") { + color = theme.palette.success.lowContrast; + icon = ( + + + + ); + } else if (title === "down") { + color = theme.palette.error.lowContrast; + icon = ( + + + + ); + } else if (title === "paused") { + color = theme.palette.warning.lowContrast; + icon = ( + + + + ); + } + + return ( + + + + + + + + {title} + + {icon} + + + {value} + + + # + + + + + ); +}; + +StatusBox.propTypes = { + title: PropTypes.oneOf(["up", "down", "paused"]).isRequired, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, +}; + +export default StatusBox; diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx new file mode 100644 index 000000000..eaf08046d --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx @@ -0,0 +1,263 @@ +// Components +import { Box, Stack, CircularProgress } from "@mui/material"; +import { Heading } from "../../../../../Components/Heading"; +import DataTable from "../../../../../Components/Table"; +import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; +import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; +import Host from "../Host"; +import { StatusLabel } from "../../../../../Components/Label"; +import BarChart from "../../../../../Components/Charts/BarChart"; +import ActionsMenu from "../ActionsMenu"; +import { useState } from "react"; +import SearchComponent from "../SearchComponent"; +import CircularCount from "../../../../../Components/CircularCount"; +import LoadingSpinner from "../LoadingSpinner"; +import UptimeDataTableSkeleton from "./skeleton"; +// Utils +import { useTheme } from "@emotion/react"; +import useUtils from "../../Hooks/useUtils"; +import { useNavigate } from "react-router-dom"; +import PropTypes from "prop-types"; + +const MonitorDataTable = ({ shouldRender, isSearching, headers, filteredMonitors }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + if (!shouldRender) return null; + return ( + + + { + navigate(`/uptime/${row.id}`); + }, + emptyView: "No monitors found", + }} + /> + + ); +}; + +/** + * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities + * @param {Object} props - Component props + * @param {boolean} props.isAdmin - Whether the current user has admin privileges + * @param {boolean} props.isLoading - Loading state of the table + * @param {Array<{ + * _id: string, + * url: string, + * title: string, + * percentage: number, + * percentageColor: string, + * monitor: { + * _id: string, + * type: string, + * checks: Array + * } + * }>} props.monitors - Array of monitor objects to display + * @param {number} props.monitorCount - Total count of monitors + * @param {Object} props.sort - Current sort configuration + * @param {string} props.sort.field - Field to sort by + * @param {'asc'|'desc'} props.sort.order - Sort direction + * @param {Function} props.setSort - Callback to update sort configuration + * @param {string} props.search - Current search query + * @param {Function} props.setSearch - Callback to update search query + * @param {boolean} props.isSearching - Whether a search is in progress + * @param {Function} props.setIsLoading - Callback to update loading state + * @param {Function} props.triggerUpdate - Callback to trigger a data refresh + * @returns {JSX.Element} Rendered component + */ +const UptimeDataTable = (props) => { + // Utils + + const { + isAdmin, + setIsLoading, + monitors, + filteredMonitors, + monitorCount, + sort, + setSort, + setSearch, + triggerUpdate, + monitorsAreLoading, + } = props; + const { determineState } = useUtils(); + const theme = useTheme(); + const navigate = useNavigate(); + + // Local state + const [isSearching, setIsSearching] = useState(false); + // Handlers + const handleSort = (field) => { + let order = ""; + if (sort.field !== field) { + order = "desc"; + } else { + order = sort.order === "asc" ? "desc" : "asc"; + } + setSort({ field, order }); + }; + + const headers = [ + { + id: "name", + content: ( + handleSort("name")} + > + Host + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + ), + render: (row) => ( + + ), + }, + { + id: "status", + content: ( + handleSort("status")} + > + {" "} + Status + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + ), + render: (row) => { + const status = determineState(row.monitor); + return ( + + ); + }, + }, + { + id: "responseTime", + content: "Response Time", + render: (row) => , + }, + { + id: "type", + content: "Type", + render: (row) => ( + {row.monitor.type} + ), + }, + { + id: "actions", + content: "Actions", + render: (row) => ( + + ), + }, + ]; + console.log("rendering"); + return ( + + + Uptime monitors + + + + + + + + ); +}; + +UptimeDataTable.propTypes = { + isSearching: PropTypes.bool, + setIsSearching: PropTypes.func, + setSort: PropTypes.func, + setSearch: PropTypes.func, + setIsLoading: PropTypes.func, + triggerUpdate: PropTypes.func, + debouncedSearch: PropTypes.string, + onSearchChange: PropTypes.func, + isAdmin: PropTypes.bool, + isLoading: PropTypes.bool, + monitors: PropTypes.array, + filteredMonitors: PropTypes.array, + monitorCount: PropTypes.number, + sort: PropTypes.shape({ + field: PropTypes.string, + order: PropTypes.oneOf(["asc", "desc"]), + }), +}; + +export default UptimeDataTable; diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx new file mode 100644 index 000000000..303eb48c8 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx @@ -0,0 +1,21 @@ +import { Skeleton } from "@mui/material"; +import PropTypes from "prop-types"; + +const UptimeDataTableSkeleton = ({ shouldRender }) => { + if (!shouldRender) return null; + + return ( + + ); +}; + +UptimeDataTableSkeleton.propTypes = { + shouldRender: PropTypes.bool.isRequired, +}; + +export default UptimeDataTableSkeleton; From 0234cab8b576a1e801db182bc04380238b936049 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 13:09:57 -0800 Subject: [PATCH 34/76] refactor uptime home page --- Client/src/Components/StatBox/index.jsx | 2 +- .../Pages/Infrastructure/Details/index.jsx | 2 +- Client/src/Pages/Infrastructure/index.jsx | 4 +- .../src/Pages/PageSpeed/Configure/index.jsx | 2 +- Client/src/Pages/PageSpeed/Details/index.jsx | 2 +- Client/src/Pages/PageSpeed/card.jsx | 2 +- Client/src/Pages/Uptime/Details/index.jsx | 2 +- .../Pages/Uptime/Home/Hooks/useDebounce.jsx | 18 + .../Uptime/Home/Hooks/useMonitorFetch.jsx | 98 ++++++ .../{utils.jsx => Home/Hooks/useUtils.jsx} | 0 Client/src/Pages/Uptime/Home/StatusBox.jsx | 106 ------ .../Home/UptimeDataTable/Skeleton/index.jsx | 47 --- .../Uptime/Home/UptimeDataTable/index.jsx | 325 ------------------ Client/src/Pages/Uptime/Home/actionsMenu.jsx | 240 ------------- Client/src/Pages/Uptime/Home/fallback.jsx | 59 ---- Client/src/Pages/Uptime/Home/host.jsx | 64 ---- Client/src/Pages/Uptime/Home/index.css | 6 - Client/src/Pages/Uptime/Home/index.jsx | 279 +++++---------- Client/src/Pages/Uptime/Home/skeleton.jsx | 61 ---- Server/controllers/monitorController.js | 2 +- Server/db/mongo/modules/monitorModule.js | 32 +- 21 files changed, 243 insertions(+), 1110 deletions(-) create mode 100644 Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx create mode 100644 Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx rename Client/src/Pages/Uptime/{utils.jsx => Home/Hooks/useUtils.jsx} (100%) delete mode 100644 Client/src/Pages/Uptime/Home/StatusBox.jsx delete mode 100644 Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx delete mode 100644 Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx delete mode 100644 Client/src/Pages/Uptime/Home/actionsMenu.jsx delete mode 100644 Client/src/Pages/Uptime/Home/fallback.jsx delete mode 100644 Client/src/Pages/Uptime/Home/host.jsx delete mode 100644 Client/src/Pages/Uptime/Home/index.css delete mode 100644 Client/src/Pages/Uptime/Home/skeleton.jsx diff --git a/Client/src/Components/StatBox/index.jsx b/Client/src/Components/StatBox/index.jsx index 953d378ec..b476a1c33 100644 --- a/Client/src/Components/StatBox/index.jsx +++ b/Client/src/Components/StatBox/index.jsx @@ -1,7 +1,7 @@ import { Box, Typography } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import useUtils from "../../Pages/Uptime/utils"; +import useUtils from "../../Pages/Uptime/Home/Hooks/useUtils"; /** * StatBox Component diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index acd1bafb1..5da921e30 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -8,7 +8,7 @@ import AreaChart from "../../../Components/Charts/AreaChart"; import { useSelector } from "react-redux"; import { networkService } from "../../../main"; import PulseDot from "../../../Components/Animated/PulseDot"; -import useUtils from "../../Uptime/utils"; +import useUtils from "../../Uptime/Home/Hooks/useUtils"; import { useNavigate } from "react-router-dom"; import Empty from "./empty"; import { logger } from "../../../Utils/Logger"; diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 0bf367b99..43768132b 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { /* useDispatch, */ useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; -import useUtils from "../Uptime/utils.jsx"; +import useUtils from "../Uptime/Home/Hooks/useUtils.jsx"; import { jwtDecode } from "jwt-decode"; import SkeletonLayout from "./skeleton"; import Fallback from "../../Components/Fallback"; @@ -17,7 +17,7 @@ import Pagination from "../../Components/Table/TablePagination/index.jsx"; // import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice"; import { networkService } from "../../Utils/NetworkService.js"; import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx"; -import Host from "../Uptime/Home/host.jsx"; +import Host from "../Uptime/Home/Components/Host"; import { useIsAdmin } from "../../Hooks/useIsAdmin.js"; import { InfrastructureMenu } from "./components/Menu"; diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx index f7829ef35..eee6dfcc9 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.jsx +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import LoadingButton from "@mui/lab/LoadingButton"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; import SkeletonLayout from "./skeleton"; -import useUtils from "../../Uptime/utils"; +import useUtils from "../../Uptime/Home/Hooks/useUtils"; import "./index.css"; import Dialog from "../../../Components/Dialog"; diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx index c5badbab8..9192faaa9 100644 --- a/Client/src/Pages/PageSpeed/Details/index.jsx +++ b/Client/src/Pages/PageSpeed/Details/index.jsx @@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import PagespeedDetailsAreaChart from "./Charts/AreaChart"; import Checkbox from "../../../Components/Inputs/Checkbox"; import PieChart from "./Charts/PieChart"; -import useUtils from "../../Uptime/utils"; +import useUtils from "../../Uptime/Home/Hooks/useUtils"; import "./index.css"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import StatBox from "../../../Components/StatBox"; diff --git a/Client/src/Pages/PageSpeed/card.jsx b/Client/src/Pages/PageSpeed/card.jsx index f5b0d7110..f7f6c4249 100644 --- a/Client/src/Pages/PageSpeed/card.jsx +++ b/Client/src/Pages/PageSpeed/card.jsx @@ -7,7 +7,7 @@ import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils"; -import useUtils from "../Uptime/utils"; +import useUtils from "../Uptime/Home/Hooks/useUtils"; import { useState } from "react"; import IconBox from "../../Components/IconBox"; /** diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx index d5be671d3..be589c1ba 100644 --- a/Client/src/Pages/Uptime/Details/index.jsx +++ b/Client/src/Pages/Uptime/Details/index.jsx @@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import { ChartBox } from "./styled"; import SkeletonLayout from "./skeleton"; import "./index.css"; -import useUtils from "../utils"; +import useUtils from "../Home/Hooks/useUtils"; import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import IconBox from "../../../Components/IconBox"; diff --git a/Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx b/Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx new file mode 100644 index 000000000..11d639f14 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx @@ -0,0 +1,18 @@ +import { useState, useEffect } from "react"; + +const useDebounce = (value, delay) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + return debouncedValue; +}; + +export default useDebounce; diff --git a/Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx new file mode 100644 index 000000000..d4b7e5ba7 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useTheme } from "@emotion/react"; +const getMonitorWithPercentage = (monitor, theme) => { + let uptimePercentage = ""; + let percentageColor = ""; + + if (monitor.uptimePercentage !== undefined) { + uptimePercentage = + monitor.uptimePercentage === 0 ? "0" : (monitor.uptimePercentage * 100).toFixed(2); + + percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.error.main + : monitor.uptimePercentage < 0.5 + ? theme.palette.warning.main + : monitor.uptimePercentage < 0.75 + ? theme.palette.success.main + : theme.palette.success.main; + } + + return { + id: monitor._id, + name: monitor.name, + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor, + monitor: monitor, + }; +}; + +export const useMonitorFetch = ({ + authToken, + teamId, + limit, + page, + rowsPerPage, + filter, + field, + order, + triggerUpdate, +}) => { + const [monitorsAreLoading, setMonitorsAreLoading] = useState(false); + const [monitors, setMonitors] = useState([]); + const [filteredMonitors, setFilteredMonitors] = useState([]); + const [monitorsSummary, setMonitorsSummary] = useState({}); + + const theme = useTheme(); + + useEffect(() => { + const fetchMonitors = async () => { + try { + setMonitorsAreLoading(true); + const res = await networkService.getMonitorsByTeamId({ + authToken, + teamId, + limit, + types: ["http", "ping", "docker", "port"], + page, + rowsPerPage, + filter, + field, + order, + }); + const { monitors, filteredMonitors, summary } = res.data.data; + const mappedMonitors = filteredMonitors.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + setMonitors(monitors); + setFilteredMonitors(mappedMonitors); + setMonitorsSummary(summary); + } catch (error) { + createToast({ + body: error.message, + }); + } finally { + setMonitorsAreLoading(false); + } + }; + fetchMonitors(); + }, [ + authToken, + teamId, + limit, + field, + filter, + order, + page, + rowsPerPage, + theme, + triggerUpdate, + ]); + return { monitors, filteredMonitors, monitorsSummary, monitorsAreLoading }; +}; + +export default useMonitorFetch; diff --git a/Client/src/Pages/Uptime/utils.jsx b/Client/src/Pages/Uptime/Home/Hooks/useUtils.jsx similarity index 100% rename from Client/src/Pages/Uptime/utils.jsx rename to Client/src/Pages/Uptime/Home/Hooks/useUtils.jsx diff --git a/Client/src/Pages/Uptime/Home/StatusBox.jsx b/Client/src/Pages/Uptime/Home/StatusBox.jsx deleted file mode 100644 index d04ebdf5f..000000000 --- a/Client/src/Pages/Uptime/Home/StatusBox.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; -import Arrow from "../../../assets/icons/top-right-arrow.svg?react"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react"; - -const StatusBox = ({ title, value }) => { - const theme = useTheme(); - - let sharedStyles = { - position: "absolute", - right: 8, - opacity: 0.5, - "& svg path": { stroke: theme.palette.primary.contrastTextTertiary }, - }; - - let color; - let icon; - if (title === "up") { - color = theme.palette.success.lowContrast; - icon = ( - - - - ); - } else if (title === "down") { - color = theme.palette.error.lowContrast; - icon = ( - - - - ); - } else if (title === "paused") { - color = theme.palette.warning.lowContrast; - icon = ( - - - - ); - } - - return ( - - - - - - {title} - - {icon} - - {value} - - - # - - - - ); -}; - -StatusBox.propTypes = { - title: PropTypes.oneOf(["up", "down", "paused"]).isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, -}; - -export default StatusBox; diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx deleted file mode 100644 index c17f6d74b..000000000 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Skeleton } from "@mui/material"; -import DataTable from "../../../../../Components/Table"; -const ROWS_NUMBER = 7; -const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); - -const TableSkeleton = () => { - /* TODO Skeleton does not follow light and dark theme */ - - const headers = [ - { - id: "name", - - content: "Host", - - render: () => , - }, - { - id: "status", - content: "Status", - render: () => , - }, - { - id: "responseTime", - content: "Response Time", - render: () => , - }, - { - id: "type", - content: "Type", - render: () => , - }, - { - id: "actions", - content: "Actions", - render: () => , - }, - ]; - - return ( - - ); -}; - -export { TableSkeleton }; diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx deleted file mode 100644 index 6961c6d5c..000000000 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ /dev/null @@ -1,325 +0,0 @@ -// Components -import { Box, Stack, CircularProgress } from "@mui/material"; -import Search from "../../../../Components/Inputs/Search"; -import { Heading } from "../../../../Components/Heading"; -import DataTable from "../../../../Components/Table"; -import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; -import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; -import Host from "../host"; -import { StatusLabel } from "../../../../Components/Label"; -import BarChart from "../../../../Components/Charts/BarChart"; -import ActionsMenu from "../actionsMenu"; - -// Utils -import { useTheme } from "@emotion/react"; -import useUtils from "../../utils"; -import { useState, memo, useCallback } from "react"; -import { useNavigate } from "react-router-dom"; -import "../index.css"; -import PropTypes from "prop-types"; - -const SearchComponent = memo( - ({ monitors, debouncedSearch, onSearchChange, setIsSearching }) => { - const [localSearch, setLocalSearch] = useState(debouncedSearch); - const handleSearch = useCallback( - (value) => { - setIsSearching(true); - setLocalSearch(value); - onSearchChange(value); - }, - [onSearchChange, setIsSearching] - ); - - return ( - - - - ); - } -); -SearchComponent.displayName = "SearchComponent"; -SearchComponent.propTypes = { - monitors: PropTypes.array, - debouncedSearch: PropTypes.string, - onSearchChange: PropTypes.func, - setIsSearching: PropTypes.func, -}; - -/** - * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities - * @param {Object} props - Component props - * @param {boolean} props.isAdmin - Whether the current user has admin privileges - * @param {boolean} props.isLoading - Loading state of the table - * @param {Array<{ - * _id: string, - * url: string, - * title: string, - * percentage: number, - * percentageColor: string, - * monitor: { - * _id: string, - * type: string, - * checks: Array - * } - * }>} props.monitors - Array of monitor objects to display - * @param {number} props.monitorCount - Total count of monitors - * @param {Object} props.sort - Current sort configuration - * @param {string} props.sort.field - Field to sort by - * @param {'asc'|'desc'} props.sort.order - Sort direction - * @param {Function} props.setSort - Callback to update sort configuration - * @param {string} props.search - Current search query - * @param {Function} props.setSearch - Callback to update search query - * @param {boolean} props.isSearching - Whether a search is in progress - * @param {Function} props.setIsSearching - Callback to update search state - * @param {Function} props.setIsLoading - Callback to update loading state - * @param {Function} props.triggerUpdate - Callback to trigger a data refresh - * @returns {JSX.Element} Rendered component - */ -const UptimeDataTable = ({ - isAdmin, - isLoading, - monitors, - filteredMonitors, - monitorCount, - sort, - setSort, - debouncedSearch, - setSearch, - isSearching, - setIsSearching, - setIsLoading, - triggerUpdate, -}) => { - const { determineState } = useUtils(); - - const theme = useTheme(); - const navigate = useNavigate(); - - const handleSort = (field) => { - let order = ""; - if (sort.field !== field) { - order = "desc"; - } else { - order = sort.order === "asc" ? "desc" : "asc"; - } - setSort({ field, order }); - }; - - const headers = [ - { - id: "name", - content: ( - handleSort("name")} - > - Host - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => ( - - ), - }, - { - id: "status", - content: ( - handleSort("status")} - > - {" "} - Status - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => { - const status = determineState(row.monitor); - return ( - - ); - }, - }, - { - id: "responseTime", - content: "Response Time", - render: (row) => , - }, - { - id: "type", - content: "Type", - render: (row) => ( - {row.monitor.type} - ), - }, - { - id: "actions", - content: "Actions", - render: (row) => ( - - ), - }, - ]; - - return ( - - - Uptime monitors - {/* TODO Same as the one in Infrastructure. Create component */} - - {monitorCount} - - - - - - {(isSearching || isLoading) && ( - <> - - - - - - )} - { - navigate(`/uptime/${row.id}`); - }, - emptyView: "No monitors found", - }} - /> - - - ); -}; - -const MemoizedUptimeDataTable = memo(UptimeDataTable); -export default MemoizedUptimeDataTable; - -UptimeDataTable.propTypes = { - isAdmin: PropTypes.bool, - isLoading: PropTypes.bool, - monitors: PropTypes.array, - filteredMonitors: PropTypes.array, - monitorCount: PropTypes.number, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.oneOf(["asc", "desc"]), - }), - setSort: PropTypes.func, - debouncedSearch: PropTypes.string, - setSearch: PropTypes.func, - isSearching: PropTypes.bool, - setIsSearching: PropTypes.func, - setIsLoading: PropTypes.func, - triggerUpdate: PropTypes.func, -}; diff --git a/Client/src/Pages/Uptime/Home/actionsMenu.jsx b/Client/src/Pages/Uptime/Home/actionsMenu.jsx deleted file mode 100644 index 493c5f25e..000000000 --- a/Client/src/Pages/Uptime/Home/actionsMenu.jsx +++ /dev/null @@ -1,240 +0,0 @@ -import { useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils"; -import { logger } from "../../../Utils/Logger"; -import { IconButton, Menu, MenuItem } from "@mui/material"; -import { - deleteUptimeMonitor, - pauseUptimeMonitor, -} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import Settings from "../../../assets/icons/settings-bold.svg?react"; -import PropTypes from "prop-types"; -import Dialog from "../../../Components/Dialog"; - -const ActionsMenu = ({ - monitor, - isAdmin, - updateRowCallback, - pauseCallback, - setIsLoading, -}) => { - const [anchorEl, setAnchorEl] = useState(null); - const [actions, setActions] = useState({}); - const [isOpen, setIsOpen] = useState(false); - const dispatch = useDispatch(); - const theme = useTheme(); - const authState = useSelector((state) => state.auth); - const authToken = authState.authToken; - const { isLoading } = useSelector((state) => state.uptimeMonitors); - - const handleRemove = async (event) => { - event.preventDefault(); - event.stopPropagation(); - let monitor = { _id: actions.id }; - const action = await dispatch( - deleteUptimeMonitor({ authToken: authState.authToken, monitor }) - ); - if (action.meta.requestStatus === "fulfilled") { - setIsOpen(false); // close modal - updateRowCallback(); - createToast({ body: "Monitor deleted successfully." }); - } else { - createToast({ body: "Failed to delete monitor." }); - } - }; - - const handlePause = async () => { - try { - setIsLoading(true); - const action = await dispatch( - pauseUptimeMonitor({ authToken, monitorId: monitor._id }) - ); - if (pauseUptimeMonitor.fulfilled.match(action)) { - const state = action?.payload?.data.isActive === false ? "paused" : "resumed"; - createToast({ body: `Monitor ${state} successfully.` }); - pauseCallback(); - } else { - throw new Error(action?.error?.message ?? "Failed to pause monitor."); - } - } catch (error) { - logger.error("Error pausing monitor:", monitor._id, error); - createToast({ body: "Failed to pause monitor." }); - } - }; - - const openMenu = (event, id, url) => { - event.preventDefault(); - event.stopPropagation(); - setAnchorEl(event.currentTarget); - setActions({ id: id, url: url }); - }; - - const openRemove = (e) => { - closeMenu(e); - setIsOpen(true); - }; - - const closeMenu = (e) => { - e.stopPropagation(); - setAnchorEl(null); - }; - - const navigate = useNavigate(); - return ( - <> - { - event.stopPropagation(); - openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url); - }} - sx={{ - "&:focus": { - outline: "none", - }, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }} - > - - - - closeMenu(e)} - disableScrollLock - slotProps={{ - paper: { - sx: { - "& ul": { - p: theme.spacing(2.5), - backgroundColor: theme.palette.primary.main, - }, - "& li": { m: 0, color: theme.palette.primary.contrastTextSecondary }, - /* - This should not be set automatically on the last of type - "& li:last-of-type": { - color: theme.palette.error.main, - }, */ - }, - }, - }} - > - {actions.url !== null ? ( - { - closeMenu(e); - e.stopPropagation(); - window.open(actions.url, "_blank", "noreferrer"); - }} - > - Open site - - ) : ( - "" - )} - { - e.stopPropagation(); - navigate(`/uptime/${actions.id}`); - }} - > - Details - - {/* TODO - pass monitor id to Incidents page */} - { - e.stopPropagation(); - navigate(`/incidents/${actions.id}`); - }} - > - Incidents - - {isAdmin && ( - { - e.stopPropagation(); - - navigate(`/uptime/configure/${actions.id}`); - }} - > - Configure - - )} - {isAdmin && ( - { - e.stopPropagation(); - navigate(`/uptime/create/${actions.id}`); - }} - > - Clone - - )} - {isAdmin && ( - { - closeMenu(e); - - e.stopPropagation(); - handlePause(e); - }} - > - {monitor?.isActive === true ? "Pause" : "Resume"} - - )} - {isAdmin && ( - { - e.stopPropagation(); - openRemove(e); - }} - sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }} - > - Remove - - )} - - { - e.stopPropagation(); - setIsOpen(false); - }} - confirmationButtonLabel="Delete" - /* Do we need stop propagation? */ - onConfirm={(e) => { - e.stopPropagation(); - handleRemove(e); - }} - isLoading={isLoading} - modelTitle="modal-delete-monitor" - modelDescription="delete-monitor-confirmation" - /> - - ); -}; - -ActionsMenu.propTypes = { - monitor: PropTypes.shape({ - _id: PropTypes.string, - url: PropTypes.string, - type: PropTypes.string, - isActive: PropTypes.bool, - }).isRequired, - isAdmin: PropTypes.bool, - updateRowCallback: PropTypes.func, - pauseCallback: PropTypes.func, - setIsLoading: PropTypes.func, -}; - -export default ActionsMenu; diff --git a/Client/src/Pages/Uptime/Home/fallback.jsx b/Client/src/Pages/Uptime/Home/fallback.jsx deleted file mode 100644 index 2606ca95f..000000000 --- a/Client/src/Pages/Uptime/Home/fallback.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Box, Button, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"; -import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react"; -import PropTypes from "prop-types"; - -const Fallback = ({ isAdmin }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const mode = useSelector((state) => state.ui.mode); - - return ( - - - {mode === "light" ? : } - - - No monitors found to display - - - It looks like you don’t have any monitors set up yet. - - {isAdmin && ( - - )} - - ); -}; - -Fallback.propTypes = { - isAdmin: PropTypes.bool, -}; - -export default Fallback; diff --git a/Client/src/Pages/Uptime/Home/host.jsx b/Client/src/Pages/Uptime/Home/host.jsx deleted file mode 100644 index ec78875f4..000000000 --- a/Client/src/Pages/Uptime/Home/host.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Box, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -/** - * Host component. - * This subcomponent receives a params object and displays the host details. - * - * @component - * @param {Object} params - An object containing the following properties: - * @param {string} params.url - The URL of the host. - * @param {string} params.title - The name of the host. - * @param {string} params.percentageColor - The color of the percentage text. - * @param {number} params.percentage - The percentage to display. - * @returns {React.ElementType} Returns a div element with the host details. - */ -const Host = ({ url, title, percentageColor, percentage }) => { - const noTitle = title === undefined || title === url; - return ( - - - {title} - - {percentageColor && percentage && ( - - {percentage}% - - )} - {!noTitle && {url}} - - ); -}; - -Host.propTypes = { - title: PropTypes.string, - percentageColor: PropTypes.string, - percentage: PropTypes.string, - url: PropTypes.string, -}; - -export default Host; diff --git a/Client/src/Pages/Uptime/Home/index.css b/Client/src/Pages/Uptime/Home/index.css deleted file mode 100644 index adb1d8674..000000000 --- a/Client/src/Pages/Uptime/Home/index.css +++ /dev/null @@ -1,6 +0,0 @@ -/* TODO take these from here and declare using emotion. Plus, the values should live in the theme */ -.monitors .MuiStack-root > button:not(.MuiIconButton-root) { - font-size: var(--env-var-font-size-medium); - height: var(--env-var-height-2); - min-width: fit-content; -} diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 446702ebb..c300118e0 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -1,128 +1,70 @@ // Components -import { Box, Stack, Button } from "@mui/material"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; import Greeting from "../../../Utils/greeting"; -import SkeletonLayout from "./skeleton"; -import Fallback from "./fallback"; -import StatusBox from "./StatusBox"; -import UptimeDataTable from "./UptimeDataTable"; +import StatusBoxes from "./Components/StatusBoxes"; +import UptimeDataTable from "./Components/UptimeDataTable"; import Pagination from "../../../Components/Table/TablePagination"; +// MUI Components +import { Stack, Box, Button } from "@mui/material"; + // Utils -import { useTheme } from "@emotion/react"; -import { useEffect, useState, useCallback, useMemo } from "react"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import { useState, useCallback } from "react"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useSelector, useDispatch } from "react-redux"; +import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import useDebounce from "../../../Utils/debounce"; -import { networkService } from "../../../main"; +import useMonitorFetch from "./Hooks/useMonitorFetch"; +import { useSelector, useDispatch } from "react-redux"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import PropTypes from "prop-types"; const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; +const CreateMonitorButton = ({ shouldRender }) => { + // Utils + const navigate = useNavigate(); + if (shouldRender === false) { + return; + } + + return ( + + + + ); +}; + +CreateMonitorButton.propTypes = { + shouldRender: PropTypes.bool, +}; + const UptimeMonitors = () => { // Redux state + const { authToken, user } = useSelector((state) => state.auth); const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage); + // Local state - const [sort, setSort] = useState({}); const [search, setSearch] = useState(""); const [page, setPage] = useState(0); + const [sort, setSort] = useState({}); const [isSearching, setIsSearching] = useState(false); const [isLoading, setIsLoading] = useState(false); const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); - const [monitors, setMonitors] = useState([]); - const [filteredMonitors, setFilteredMonitors] = useState([]); - const [monitorsSummary, setMonitorsSummary] = useState({}); // Utils - const debouncedFilter = useDebounce(search, 500); - const dispatch = useDispatch(); const theme = useTheme(); - const navigate = useNavigate(); const isAdmin = useIsAdmin(); + const dispatch = useDispatch(); - const authState = useSelector((state) => state.auth); - - const fetchParams = useMemo( - () => ({ - authToken: authState.authToken, - teamId: authState.user.teamId, - sort: { field: sort.field, order: sort.order }, - filter: debouncedFilter, - page, - rowsPerPage, - }), - [authState.authToken, authState.user.teamId, sort, debouncedFilter, page, rowsPerPage] - ); - - const getMonitorWithPercentage = useCallback((monitor, theme) => { - let uptimePercentage = ""; - let percentageColor = ""; - - if (monitor.uptimePercentage !== undefined) { - uptimePercentage = - monitor.uptimePercentage === 0 - ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); - - percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.error.main - : monitor.uptimePercentage < 0.5 - ? theme.palette.warning.main - : monitor.uptimePercentage < 0.75 - ? theme.palette.success.main - : theme.palette.success.main; - } - - return { - id: monitor._id, - name: monitor.name, - url: monitor.url, - title: monitor.name, - percentage: uptimePercentage, - percentageColor, - monitor: monitor, - }; - }, []); - - const fetchMonitors = useCallback(async () => { - try { - setIsLoading(true); - const config = fetchParams; - const res = await networkService.getMonitorsByTeamId({ - authToken: config.authToken, - teamId: config.teamId, - limit: 25, - types: ["http", "ping", "docker", "port"], - page: config.page, - rowsPerPage: config.rowsPerPage, - filter: config.filter, - field: config.sort.field, - order: config.sort.order, - }); - const { monitors, filteredMonitors, summary } = res.data.data; - const mappedMonitors = filteredMonitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - setMonitors(monitors); - setFilteredMonitors(mappedMonitors); - setMonitorsSummary(summary); - } catch (error) { - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - setIsSearching(false); - } - }, [fetchParams, getMonitorWithPercentage, theme]); - - useEffect(() => { - fetchMonitors(); - }, [fetchMonitors, monitorUpdateTrigger]); - + // Handlers const handleChangePage = (event, newPage) => { setPage(newPage); }; @@ -140,103 +82,58 @@ const UptimeMonitors = () => { const triggerUpdate = useCallback(() => { setMonitorUpdateTrigger((prev) => !prev); }, []); + + const teamId = user.teamId; + + const { monitorsAreLoading, monitors, filteredMonitors, monitorsSummary } = + useMonitorFetch({ + authToken, + teamId, + limit: 25, + page, + rowsPerPage: rowsPerPage, + filter: search, + field: sort.field, + order: sort.order, + triggerUpdate: monitorUpdateTrigger, + }); const totalMonitors = monitorsSummary?.totalMonitors ?? 0; - const hasMonitors = totalMonitors > 0; - const canAddMonitor = isAdmin && hasMonitors; return ( - - - - {canAddMonitor && ( - <> - - - )} - - - - { - <> - {!isLoading && !hasMonitors && } - {isLoading ? ( - - ) : ( - hasMonitors && ( - <> - - - - - - - - - ) - )} - - } + + + + + + ); }; diff --git a/Client/src/Pages/Uptime/Home/skeleton.jsx b/Client/src/Pages/Uptime/Home/skeleton.jsx deleted file mode 100644 index 4e55be536..000000000 --- a/Client/src/Pages/Uptime/Home/skeleton.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 86c0fb840..ccd8ec26f 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -436,7 +436,7 @@ class MonitorController { monitor.isActive = !monitor.isActive; monitor.status = undefined; monitor.save(); - return res.ssuccess({ + return res.success({ msg: monitor.isActive ? successMessages.MONITOR_RESUME : successMessages.MONITOR_PAUSE, diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 4c60e0f22..cad9bd68f 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -497,7 +497,7 @@ const getMonitorById = async (monitorId) => { const getMonitorsByTeamId = async (req) => { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - + console.log("req.query", req.query); limit = parseInt(limit); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); @@ -512,8 +512,16 @@ const getMonitorsByTeamId = async (req) => { } const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; + + console.log("limit", limit); + console.log("page", page); + console.log("rowsPerPage", rowsPerPage); + console.log("filter", filter); + console.log("field", field); + console.log("order", order); + console.log("skip", skip); + console.log("sort", sort); const results = await Monitor.aggregate([ { $match: matchStage }, { @@ -675,6 +683,7 @@ const getMonitorsByTeamId = async (req) => { ]); let { monitors, filteredMonitors, summary } = results[0]; + console.log("filteredMonitors", filteredMonitors); filteredMonitors = filteredMonitors.map((monitor) => { if (!monitor.checks) { return monitor; @@ -839,3 +848,22 @@ export { groupChecksByTime, calculateGroupStats, }; + +// limit 25 +// page 1 +// rowsPerPage 25 +// filter undefined +// field name +// order asc +// skip 25 +// sort { name: 1 } +// filteredMonitors [] + +// limit 25 +// page NaN +// rowsPerPage 25 +// filter undefined +// field name +// order asc +// skip 0 +// sort { name: 1 } From be6d2f25ea29e984d43088ec33cc865a68333448 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 16:22:14 -0800 Subject: [PATCH 35/76] rationalize host component --- .../Uptime/Home/Components/Host/index.jsx | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx index ec78875f4..9238f9889 100644 --- a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx +++ b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx @@ -1,5 +1,6 @@ -import { Box, Typography } from "@mui/material"; +import { Stack, Box, Typography } from "@mui/material"; import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; /** * Host component. * This subcomponent receives a params object and displays the host details. @@ -13,44 +14,43 @@ import PropTypes from "prop-types"; * @returns {React.ElementType} Returns a div element with the host details. */ const Host = ({ url, title, percentageColor, percentage }) => { - const noTitle = title === undefined || title === url; + const theme = useTheme(); + console.log(url, title); return ( - - + {title} - - {percentageColor && percentage && ( - - {percentage}% - - )} - {!noTitle && {url}} - + {percentageColor && percentage && ( + <> + + + {percentage}% + + + )} + + {url} + ); }; From 3024c205463339e3fc7d6aa1a9dc44d05cb1b8ea Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:30:32 -0500 Subject: [PATCH 36/76] Better pull request template --- .github/pull_request_template.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 64d8d8ef1..402f5b15e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ -**(when sending your PR, only remove this line. Do not remove any lines below. Check all the relevant items before sending your PR)** +**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)** ## Describe your changes @@ -8,10 +8,10 @@ Briefly describe the changes you made and their purpose. Mention the issue number(s) this PR addresses (e.g., #123). -## Please ensure all items are checked off before requesting a review: +## Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks. -- [ ] I deployed the application locally. -- [ ] I have performed a self-review and testing of my code. +- [ ] (Do not skip this or your PR will be closed) I deployed the application locally. +- [ ] (Do not skip this or your PR will be closed) I have performed a self-review and testing of my code. - [ ] I have included the issue # in the PR. - [ ] I have labelled the PR correctly. - [ ] The issue I am working on is assigned to me. From 6d1f085f743048e9abb1eaf2afe69df5b79ff48d Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sun, 26 Jan 2025 10:11:45 +0000 Subject: [PATCH 37/76] fix(deps): update dependency bullmq to v5.37.0 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index 67399c8a1..17dab07b0 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.35.1", + "bullmq": "5.37.0", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", @@ -1748,9 +1748,9 @@ } }, "node_modules/bullmq": { - "version": "5.35.1", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.35.1.tgz", - "integrity": "sha512-LnzqisWyF4V8KhgFnK7iaJdQn9Q61EdHdCT6sVTzA3jZr6CHxw/OBmBZhzA8Xq4bsvsmvdjr8pmGftC78aWgXQ==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.37.0.tgz", + "integrity": "sha512-h/wf979+9uROyYpB8oTE44Py6JERCluCSd+ZFpCZlPsYh+wxAkqrfHsHHHKBgsNJp9odWLIY4SG+280EXzXiCQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", diff --git a/Server/package.json b/Server/package.json index 873f8ce67..9d92a2010 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.35.1", + "bullmq": "5.37.0", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", From 2818ce89affefd3c8db6cd36094de8b705bc354c Mon Sep 17 00:00:00 2001 From: YinDongfang <1136005348@qq.com> Date: Sun, 26 Jan 2025 22:32:04 +0800 Subject: [PATCH 38/76] Adjust count box style --- Client/src/Pages/Infrastructure/index.jsx | 2 +- Client/src/Pages/PageSpeed/index.jsx | 10 ++++++++-- Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index fbfae6a3c..e39d92347 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -243,7 +243,7 @@ function Infrastructure() { backgroundColor={theme.palette.tertiary.main} sx={{ padding: ".25em .75em", - borderRadius: "50%", + borderRadius: "calc(infinity * 1px)", fontSize: "12px", fontWeight: 500, }} diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index c15ed5296..7fb45d479 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -106,9 +106,15 @@ const PageSpeed = () => { component="span" className="current-monitors-counter" color={theme.palette.primary.contrastText} - border={1} - borderColor={theme.palette.primary.lowContrast} + border={2} + borderColor={theme.palette.accent.main} backgroundColor={theme.palette.tertiary.main} + sx={{ + padding: ".25em .75em", + borderRadius: "calc(infinity * 1px)", + fontSize: "12px", + fontWeight: 500, + }} > {summary?.totalMonitors ?? 0} diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx index 6961c6d5c..e108f6904 100644 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx @@ -234,7 +234,7 @@ const UptimeDataTable = ({ backgroundColor={theme.palette.tertiary.main} sx={{ padding: ".25em .75em", - borderRadius: "50%", + borderRadius: "calc(infinity * 1px)", fontSize: "12px", fontWeight: 500, }} From 086a900613b838fb9bec5bde35b5ac9fe6bf9a25 Mon Sep 17 00:00:00 2001 From: YinDongfang <1136005348@qq.com> Date: Sun, 26 Jan 2025 22:48:45 +0800 Subject: [PATCH 39/76] Fix Error Component Stack --- Client/src/Pages/Infrastructure/index.jsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index e39d92347..0246537df 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -137,13 +137,11 @@ function Infrastructure() { id: "actions", content: "Actions", render: (row) => ( - - - + ), }, ]; From 000bb9929cfce4da1c366931e053d11185b04fb7 Mon Sep 17 00:00:00 2001 From: YinDongfang <1136005348@qq.com> Date: Sun, 26 Jan 2025 23:05:18 +0800 Subject: [PATCH 40/76] Fix sidebar sticky error --- Client/src/Pages/Infrastructure/Details/index.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index acd1bafb1..a4270297d 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -625,8 +625,7 @@ const InfrastructureDetails = () => {
Date: Sun, 26 Jan 2025 21:37:52 +0000 Subject: [PATCH 41/76] fix(deps): update dependency @reduxjs/toolkit to v2.5.1 --- Client/package-lock.json | 8 ++++---- Client/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index abbc57323..7d2a2427c 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -17,7 +17,7 @@ "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.24.1", "@mui/x-date-pickers": "7.24.1", - "@reduxjs/toolkit": "2.5.0", + "@reduxjs/toolkit": "2.5.1", "axios": "^1.7.4", "dayjs": "1.11.13", "joi": "17.13.3", @@ -1691,9 +1691,9 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", - "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz", + "integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==", "license": "MIT", "dependencies": { "immer": "^10.0.3", diff --git a/Client/package.json b/Client/package.json index 006bdd081..1e2647a03 100644 --- a/Client/package.json +++ b/Client/package.json @@ -20,7 +20,7 @@ "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.24.1", "@mui/x-date-pickers": "7.24.1", - "@reduxjs/toolkit": "2.5.0", + "@reduxjs/toolkit": "2.5.1", "axios": "^1.7.4", "dayjs": "1.11.13", "joi": "17.13.3", From 3193ede1b192a9e2904e1de733632a9da18c31eb Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:15:00 -0500 Subject: [PATCH 42/76] Add port monitoring --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c37adfc6a..3dd59ef8f 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ If you have any questions, suggestions or comments, please use our [Discord chan - Docker monitoring - Ping monitoring - SSL monitoring +- Port monitoring - Incidents at a glance - E-mail notifications - Scheduled maintenance **Short term roadmap:** -- Port monitoring (**complete**, waiting to be deployed to stable version) https://github.com/bluewave-labs/Checkmate/issues/1476 - Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593 - Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131 - Translations (i18n) (**in progress**) From 9a250e14603e29731145bba8d6f120c21470401d Mon Sep 17 00:00:00 2001 From: YinDongfang <1136005348@qq.com> Date: Mon, 27 Jan 2025 08:21:46 +0800 Subject: [PATCH 43/76] Use 10000px instead of infinity --- Client/src/Pages/Infrastructure/index.jsx | 2 +- Client/src/Pages/PageSpeed/index.jsx | 2 +- Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 0246537df..f9abb16f3 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -241,7 +241,7 @@ function Infrastructure() { backgroundColor={theme.palette.tertiary.main} sx={{ padding: ".25em .75em", - borderRadius: "calc(infinity * 1px)", + borderRadius: "10000px", fontSize: "12px", fontWeight: 500, }} diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index 7fb45d479..c3d37e2c0 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -111,7 +111,7 @@ const PageSpeed = () => { backgroundColor={theme.palette.tertiary.main} sx={{ padding: ".25em .75em", - borderRadius: "calc(infinity * 1px)", + borderRadius: "10000px", fontSize: "12px", fontWeight: 500, }} diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx index e108f6904..29d276159 100644 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx @@ -234,7 +234,7 @@ const UptimeDataTable = ({ backgroundColor={theme.palette.tertiary.main} sx={{ padding: ".25em .75em", - borderRadius: "calc(infinity * 1px)", + borderRadius: "10000px", fontSize: "12px", fontWeight: 500, }} From aa0f0621f909b1e5ff2cb16379b8b0e0583b3932 Mon Sep 17 00:00:00 2001 From: Jasneet Singh Date: Sun, 26 Jan 2025 20:50:12 -0500 Subject: [PATCH 44/76] Move "Checks to perform" to the top of "General settings" --- .../src/Pages/Uptime/CreateUptime/index.jsx | 93 +++++++++---------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/Client/src/Pages/Uptime/CreateUptime/index.jsx b/Client/src/Pages/Uptime/CreateUptime/index.jsx index 762d8e764..9086c87af 100644 --- a/Client/src/Pages/Uptime/CreateUptime/index.jsx +++ b/Client/src/Pages/Uptime/CreateUptime/index.jsx @@ -235,53 +235,6 @@ const CreateMonitor = () => { monitor - - - - General settings - - Here you can select the URL of the host, together with the type of monitor. - - - - : null - } - label={monitorTypeMaps[monitor.type].label || "URL to monitor"} - https={https} - placeholder={monitorTypeMaps[monitor.type].placeholder || ""} - value={monitor.url} - onChange={(event) => handleChange(event, "url")} - error={errors["url"] ? true : false} - helperText={errors["url"]} - /> - handleChange(event, "port")} - error={errors["port"] ? true : false} - helperText={errors["port"]} - hidden={monitor.type !== "port"} - /> - handleChange(event, "name")} - error={errors["name"] ? true : false} - helperText={errors["name"]} - /> - - Checks to perform @@ -363,6 +316,52 @@ const CreateMonitor = () => { )} + + + General settings + + Here you can select the URL of the host, together with the type of monitor. + + + + : null + } + label={monitorTypeMaps[monitor.type].label || "URL to monitor"} + https={https} + placeholder={monitorTypeMaps[monitor.type].placeholder || ""} + value={monitor.url} + onChange={(event) => handleChange(event, "url")} + error={errors["url"] ? true : false} + helperText={errors["url"]} + /> + handleChange(event, "port")} + error={errors["port"] ? true : false} + helperText={errors["port"]} + hidden={monitor.type !== "port"} + /> + handleChange(event, "name")} + error={errors["name"] ? true : false} + helperText={errors["name"]} + /> + + Incident notifications From 22cae67ffef202892d0e4cb235a99232645dc8e0 Mon Sep 17 00:00:00 2001 From: YinDongfang <1136005348@qq.com> Date: Mon, 27 Jan 2025 11:37:44 +0800 Subject: [PATCH 45/76] Fix redundant instantiation of NetworkService --- Client/src/Utils/NetworkServiceProvider.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Client/src/Utils/NetworkServiceProvider.jsx b/Client/src/Utils/NetworkServiceProvider.jsx index 9fb65f1ca..12eab48a9 100644 --- a/Client/src/Utils/NetworkServiceProvider.jsx +++ b/Client/src/Utils/NetworkServiceProvider.jsx @@ -1,14 +1,16 @@ import { useDispatch } from "react-redux"; import { useNavigate } from "react-router"; import { setNetworkService } from "./NetworkService"; -import NetworkService from "./NetworkService"; +import NetworkService, { networkService } from "./NetworkService"; import { store } from "../store"; const NetworkServiceProvider = ({ children }) => { const dispatch = useDispatch(); const navigate = useNavigate(); - const networkService = new NetworkService(store, dispatch, navigate); - setNetworkService(networkService); + if (!networkService) { + const networkService = new NetworkService(store, dispatch, navigate); + setNetworkService(networkService); + } return children; }; From 7c889dde4788d904218ba90f612e30348a0e7905 Mon Sep 17 00:00:00 2001 From: YinDongfang <1136005348@qq.com> Date: Mon, 27 Jan 2025 12:21:46 +0800 Subject: [PATCH 46/76] Avoid shadow the networkService --- Client/src/Utils/NetworkServiceProvider.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Client/src/Utils/NetworkServiceProvider.jsx b/Client/src/Utils/NetworkServiceProvider.jsx index 12eab48a9..684152e69 100644 --- a/Client/src/Utils/NetworkServiceProvider.jsx +++ b/Client/src/Utils/NetworkServiceProvider.jsx @@ -8,8 +8,7 @@ const NetworkServiceProvider = ({ children }) => { const dispatch = useDispatch(); const navigate = useNavigate(); if (!networkService) { - const networkService = new NetworkService(store, dispatch, navigate); - setNetworkService(networkService); + setNetworkService(new NetworkService(store, dispatch, navigate)); } return children; }; From e19dffb186d61594732a1ed56687275cb4003d3c Mon Sep 17 00:00:00 2001 From: YinDongfang <1136005348@qq.com> Date: Mon, 27 Jan 2025 12:50:53 +0800 Subject: [PATCH 47/76] Fix infrastructure white page error cause by theme color --- Client/src/Pages/Infrastructure/index.jsx | 14 ++++++-------- Client/src/Pages/Uptime/Home/index.jsx | 4 +--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index f9abb16f3..869d0d13c 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -156,14 +156,12 @@ function Infrastructure() { const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100) .toFixed(2) .toString(); - const percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.error.main - : monitor.uptimePercentage < 0.5 - ? theme.palette.percentage.uptimeFair - : monitor.uptimePercentage < 0.75 - ? theme.palette.percentage.uptimeGood - : theme.palette.success.lowContrast; + const percentageColor = monitor.uptimePercentage < 0.25 + ? theme.palette.error.main + : monitor.uptimePercentage < 0.5 + ? theme.palette.warning.main + : theme.palette.success.main; + return { id: monitor._id, name: monitor.name, diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 446702ebb..ae4213a7f 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -71,9 +71,7 @@ const UptimeMonitors = () => { ? theme.palette.error.main : monitor.uptimePercentage < 0.5 ? theme.palette.warning.main - : monitor.uptimePercentage < 0.75 - ? theme.palette.success.main - : theme.palette.success.main; + : theme.palette.success.main; } return { From 4624a51a80a55401fdf93f6f6c684a244be576d5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 07:11:19 -0800 Subject: [PATCH 48/76] remove console.log --- Client/src/Pages/Uptime/Home/Components/Host/index.jsx | 1 - .../src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx | 1 - 2 files changed, 2 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx index 9238f9889..91b7c418b 100644 --- a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx +++ b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx @@ -15,7 +15,6 @@ import { useTheme } from "@emotion/react"; */ const Host = ({ url, title, percentageColor, percentage }) => { const theme = useTheme(); - console.log(url, title); return ( { ), }, ]; - console.log("rendering"); return ( Date: Mon, 27 Jan 2025 07:15:07 -0800 Subject: [PATCH 49/76] remove console log --- Server/controllers/distributedUptimeController.js | 1 - Server/db/mongo/modules/monitorModule.js | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/Server/controllers/distributedUptimeController.js b/Server/controllers/distributedUptimeController.js index ce4ee2df5..5215c2cce 100644 --- a/Server/controllers/distributedUptimeController.js +++ b/Server/controllers/distributedUptimeController.js @@ -8,7 +8,6 @@ class DistributedUptimeController { async resultsCallback(req, res, next) { try { - console.log(req.body); res.status(200).json({ message: "OK" }); } catch (error) { throw handleError(error, SERVICE_NAME, "resultsCallback"); diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index cad9bd68f..fdbd86477 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -497,7 +497,6 @@ const getMonitorById = async (monitorId) => { const getMonitorsByTeamId = async (req) => { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - console.log("req.query", req.query); limit = parseInt(limit); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); @@ -513,15 +512,6 @@ const getMonitorsByTeamId = async (req) => { const skip = page && rowsPerPage ? page * rowsPerPage : 0; const sort = { [field]: order === "asc" ? 1 : -1 }; - - console.log("limit", limit); - console.log("page", page); - console.log("rowsPerPage", rowsPerPage); - console.log("filter", filter); - console.log("field", field); - console.log("order", order); - console.log("skip", skip); - console.log("sort", sort); const results = await Monitor.aggregate([ { $match: matchStage }, { From 3259425c64f8b2405649cd9188f775ee6c684384 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 07:18:02 -0800 Subject: [PATCH 50/76] update border radius of circular count --- Client/src/Components/CircularCount/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Components/CircularCount/index.jsx b/Client/src/Components/CircularCount/index.jsx index 5c09ad162..8350a8c44 100644 --- a/Client/src/Components/CircularCount/index.jsx +++ b/Client/src/Components/CircularCount/index.jsx @@ -12,7 +12,7 @@ const CircularCount = ({ count }) => { backgroundColor={theme.palette.tertiary.main} sx={{ padding: ".25em .75em", - borderRadius: "50%", + borderRadius: "50rem", fontSize: "12px", fontWeight: 500, }} From 65b104f38ab318bdeac5acbb3e604792a2951c94 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 07:27:53 -0800 Subject: [PATCH 51/76] remove console log, add back commented out container height --- Client/src/Pages/Infrastructure/Details/index.jsx | 3 ++- Server/db/mongo/modules/monitorModule.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index cb408b9f2..07fbd7c06 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -625,7 +625,8 @@ const InfrastructureDetails = () => { { ]); let { monitors, filteredMonitors, summary } = results[0]; - console.log("filteredMonitors", filteredMonitors); filteredMonitors = filteredMonitors.map((monitor) => { if (!monitor.checks) { return monitor; From dba266bcd2d826897513e6e89eff1c6d8fe6dd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20=C5=9Ei=C5=9Fmano=C4=9Flu?= Date: Mon, 27 Jan 2025 19:25:35 +0300 Subject: [PATCH 52/76] chore(npm): Remove package-lock.json and add '.npmrc' to disable lockfile generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Şişmanoğlu --- .npmrc | 1 + package-lock.json | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) create mode 100644 .npmrc delete mode 100644 package-lock.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..43c97e719 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index f71a78c1f..000000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "bluewave-uptime", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} From 7292af1902135a734636a14459b90d4c06e58fd1 Mon Sep 17 00:00:00 2001 From: Aryaman Kumar Sharma Date: Mon, 27 Jan 2025 22:48:14 +0530 Subject: [PATCH 53/76] Fix: Update UptimeDataTable to reflect HTTP(s) monitor types --- .../Pages/Uptime/Home/Components/UptimeDataTable/index.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx index 4ab605587..936a6ebed 100644 --- a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx @@ -190,7 +190,9 @@ const UptimeDataTable = (props) => { id: "type", content: "Type", render: (row) => ( - {row.monitor.type} + + {row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type} + ), }, { From d65a5d001a58ed3bff553fba2c5c169ce10f586e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 09:36:39 -0800 Subject: [PATCH 54/76] Add status param to getChecks requests --- Client/src/Utils/NetworkService.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index c73890218..d040ff2fd 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -614,6 +614,7 @@ class NetworkService { if (config.filter) params.append("filter", config.filter); if (config.page) params.append("page", config.page); if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage); + if (config.status !== undefined) params.append("status", config.status); return this.axiosInstance.get(`/checks/${config.monitorId}?${params.toString()}`, { headers: { Authorization: `Bearer ${config.authToken}` }, @@ -646,6 +647,7 @@ class NetworkService { if (config.filter) params.append("filter", config.filter); if (config.page) params.append("page", config.page); if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage); + if (config.status !== undefined) params.append("status", config.status); return this.axiosInstance.get(`/checks/team/${config.teamId}?${params.toString()}`, { headers: { Authorization: `Bearer ${config.authToken}` }, }); From c7c9835e9c4d0236468f1077c43d23228b865908 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 09:36:52 -0800 Subject: [PATCH 55/76] Add validation for status param --- Server/validation/joi.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 8f366a113..9f20eb0d1 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -292,6 +292,7 @@ const getChecksQueryValidation = joi.object({ filter: joi.string().valid("all", "down", "resolve"), page: joi.number(), rowsPerPage: joi.number(), + status: joi.boolean(), }); const getTeamChecksParamValidation = joi.object({ @@ -305,6 +306,7 @@ const getTeamChecksQueryValidation = joi.object({ filter: joi.string().valid("all", "down", "resolve"), page: joi.number(), rowsPerPage: joi.number(), + status: joi.boolean(), }); const deleteChecksParamValidation = joi.object({ From cb624f1f294259877d25297953d27fd4482b4b69 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 09:37:29 -0800 Subject: [PATCH 56/76] conditionally add status to the match stage of the aggreagation pipeline --- Server/db/mongo/modules/checkModule.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 7965e7c30..3d158a812 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -70,13 +70,14 @@ const createCheck = async (checkData) => { const getChecksByMonitor = async (req) => { try { const { monitorId } = req.params; - let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query; + let { sortOrder, dateRange, filter, page, rowsPerPage, status } = req.query; + status = typeof status !== "undefined" ? false : undefined; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); // Match const matchStage = { monitorId: ObjectId.createFromHexString(monitorId), - status: false, + ...(typeof status !== "undefined" && { status }), ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], @@ -111,7 +112,6 @@ const getChecksByMonitor = async (req) => { if (page && rowsPerPage) { skip = page * rowsPerPage; } - const checks = await Check.aggregate([ { $match: matchStage }, { $sort: { createdAt: sortOrder } }, From 875c13b5feb92d5d8506dcdb02daa801c3bff8c4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 09:37:39 -0800 Subject: [PATCH 57/76] Update incident table queries --- Client/src/Pages/Incidents/IncidentTable/index.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index ae9a3ae2a..15d65cadd 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -39,6 +39,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter, dateRange }) => { if (selectedMonitor === "0") { res = await networkService.getChecksByTeam({ authToken: authToken, + status: false, teamId: user.teamId, sortOrder: "desc", limit: null, @@ -50,6 +51,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter, dateRange }) => { } else { res = await networkService.getChecksByMonitor({ authToken: authToken, + status: false, monitorId: selectedMonitor, sortOrder: "desc", limit: null, From 94084acd865a32ea93109f1c03689475c87a53e6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 10:00:58 -0800 Subject: [PATCH 58/76] iniital commit --- ...eMonitorFetch.jsx => useMonitorsFetch.jsx} | 0 Client/src/Pages/Uptime/Home/index.jsx | 4 +- .../NewDetails/Hooks/useCertificateFetch.jsx | 45 +++++++++++++++++ .../NewDetails/Hooks/useMonitorFetch.jsx | 34 +++++++++++++ Client/src/Pages/Uptime/NewDetails/index.jsx | 48 +++++++++++++++++++ Client/src/Routes/index.jsx | 4 +- 6 files changed, 132 insertions(+), 3 deletions(-) rename Client/src/Pages/Uptime/Home/Hooks/{useMonitorFetch.jsx => useMonitorsFetch.jsx} (100%) create mode 100644 Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/Home/Hooks/useMonitorsFetch.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx rename to Client/src/Pages/Uptime/Home/Hooks/useMonitorsFetch.jsx diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index c300118e0..2427d7e61 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -13,7 +13,7 @@ import { useState, useCallback } from "react"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import useMonitorFetch from "./Hooks/useMonitorFetch"; +import useMonitorsFetch from "./Hooks/useMonitorsFetch"; import { useSelector, useDispatch } from "react-redux"; import { setRowsPerPage } from "../../../Features/UI/uiSlice"; import PropTypes from "prop-types"; @@ -86,7 +86,7 @@ const UptimeMonitors = () => { const teamId = user.teamId; const { monitorsAreLoading, monitors, filteredMonitors, monitorsSummary } = - useMonitorFetch({ + useMonitorsFetch({ authToken, teamId, limit: 25, diff --git a/Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx b/Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx new file mode 100644 index 000000000..33d2d71c5 --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx @@ -0,0 +1,45 @@ +import { logger } from "../../../../Utils/Logger"; +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { formatDateWithTz } from "../../../../Utils/DateUtils"; + +const useCertificateFetch = ({ + monitor, + authToken, + monitorId, + certificateDateFormat, + uiTimezone, +}) => { + const [certificateExpiry, setCertificateExpiry] = useState("N/A"); + const [certificateIsLoading, setCertificateIsLoading] = useState(false); + + useEffect(() => { + const fetchCertificate = async () => { + if (monitor?.type !== "http") { + return; + } + + try { + setCertificateIsLoading(true); + const res = await networkService.getCertificateExpiry({ + authToken: authToken, + monitorId: monitorId, + }); + if (res?.data?.data?.certificateDate) { + const date = res.data.data.certificateDate; + setCertificateExpiry( + formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A" + ); + } + } catch (error) { + setCertificateExpiry("N/A"); + logger.error(error); + } finally { + setCertificateIsLoading(false); + } + }; + }, [authToken, monitorId]); + return { certificateExpiry, certificateIsLoading }; +}; + +export default useCertificateFetch; diff --git a/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx new file mode 100644 index 000000000..e7ed825f2 --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { logger } from "../../../../Utils/Logger"; +import { useNavigate } from "react-router-dom"; + +export const useMonitorFetch = ({ authToken, monitorId, dateRange }) => { + const [monitorIsLoading, setMonitorsIsLoading] = useState(false); + const [monitor, setMonitor] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + const fetchMonitors = async () => { + try { + setMonitorsIsLoading(true); + const res = await networkService.getUptimeDetailsById({ + authToken: authToken, + monitorId: monitorId, + dateRange: dateRange, + normalize: true, + }); + setMonitor(res?.data?.data ?? {}); + } catch (error) { + logger.error(error); + navigate("/not-found", { replace: true }); + } finally { + setMonitorsIsLoading(false); + } + }; + fetchMonitors(); + }, [authToken, dateRange, monitorId, navigate]); + return { monitor, monitorIsLoading }; +}; + +export default useMonitorFetch; diff --git a/Client/src/Pages/Uptime/NewDetails/index.jsx b/Client/src/Pages/Uptime/NewDetails/index.jsx new file mode 100644 index 000000000..3467f8228 --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/index.jsx @@ -0,0 +1,48 @@ +// Components +import Breadcrumbs from "../../../Components/Breadcrumbs"; + +// MUI Components +import { Stack } from "@mui/material"; + +// Utils +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useMonitorFetch } from "./Hooks/useMonitorFetch"; +// Constants +const BREADCRUMBS = [ + { name: "uptime", path: "/uptime" }, + { name: "details", path: "" }, + // { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy +]; + +const certificateDateFormat = "MMM D, YYYY h A"; + +const UptimeDetails = () => { + // Redux state + const { authToken } = useSelector((state) => state.auth); + const uiTimezone = useSelector((state) => state.ui.timezone); + + // Local state + const [dateRange, setDateRange] = useState("day"); + + // Utils + const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; + const { monitorId } = useParams(); + + const { monitor, monitorIsLoading } = useMonitorFetch({ + authToken, + monitorId, + dateRange, + }); + + console.log(monitor); + + return ( + + + + ); +}; + +export default UptimeDetails; diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx index b5f4dd376..6ac5624a9 100644 --- a/Client/src/Routes/index.jsx +++ b/Client/src/Routes/index.jsx @@ -19,6 +19,8 @@ import SetNewPassword from "../Pages/Auth/SetNewPassword"; import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; import ProtectedRoute from "../Components/ProtectedRoute"; import Details from "../Pages/Uptime/Details"; + +import UptimeDetails from "../Pages/Uptime/NewDetails"; import Maintenance from "../Pages/Maintenance"; import Configure from "../Pages/Uptime/Configure"; import PageSpeed from "../Pages/PageSpeed"; @@ -55,7 +57,7 @@ const Routes = () => { /> } + element={} /> Date: Mon, 27 Jan 2025 10:05:17 -0800 Subject: [PATCH 59/76] Add certificate expiry hook --- .../NewDetails/Hooks/useCertificateFetch.jsx | 5 +++-- Client/src/Pages/Uptime/NewDetails/index.jsx | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx b/Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx index 33d2d71c5..9e1c3b44f 100644 --- a/Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx +++ b/Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx @@ -1,7 +1,7 @@ import { logger } from "../../../../Utils/Logger"; import { useEffect, useState } from "react"; import { networkService } from "../../../../main"; -import { formatDateWithTz } from "../../../../Utils/DateUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtils"; const useCertificateFetch = ({ monitor, @@ -38,7 +38,8 @@ const useCertificateFetch = ({ setCertificateIsLoading(false); } }; - }, [authToken, monitorId]); + fetchCertificate(); + }, [authToken, monitorId, certificateDateFormat, uiTimezone, monitor]); return { certificateExpiry, certificateIsLoading }; }; diff --git a/Client/src/Pages/Uptime/NewDetails/index.jsx b/Client/src/Pages/Uptime/NewDetails/index.jsx index 3467f8228..5494146e6 100644 --- a/Client/src/Pages/Uptime/NewDetails/index.jsx +++ b/Client/src/Pages/Uptime/NewDetails/index.jsx @@ -5,10 +5,11 @@ import Breadcrumbs from "../../../Components/Breadcrumbs"; import { Stack } from "@mui/material"; // Utils -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; -import { useMonitorFetch } from "./Hooks/useMonitorFetch"; +import useMonitorFetch from "./Hooks/useMonitorFetch"; +import useCertificateFetch from "./Hooks/useCertificateFetch"; // Constants const BREADCRUMBS = [ { name: "uptime", path: "/uptime" }, @@ -36,8 +37,16 @@ const UptimeDetails = () => { dateRange, }); - console.log(monitor); + const { certificateExpiry, certificateIsLoading } = useCertificateFetch({ + monitor, + authToken, + monitorId, + certificateDateFormat, + uiTimezone, + }); + console.log(monitor); + console.log(certificateExpiry); return ( From 66e61fc8be568679fc930e4f30c802facaa2c086 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 13:16:26 -0800 Subject: [PATCH 60/76] Add Dot component --- Client/src/Components/Dot/index.jsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Client/src/Components/Dot/index.jsx diff --git a/Client/src/Components/Dot/index.jsx b/Client/src/Components/Dot/index.jsx new file mode 100644 index 000000000..0fd51730f --- /dev/null +++ b/Client/src/Components/Dot/index.jsx @@ -0,0 +1,23 @@ +import PropTypes from "prop-types"; + +const Dot = ({ color = "gray", size = "4px" }) => { + return ( + + ); +}; + +Dot.propTypes = { + color: PropTypes.string, + size: PropTypes.string, +}; + +export default Dot; From 5622b3b47eeb5f96bf7e3972b99f47b2ead9aded Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 13:17:05 -0800 Subject: [PATCH 61/76] use dayjs to parse dates --- Client/src/Utils/timeUtils.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Client/src/Utils/timeUtils.js b/Client/src/Utils/timeUtils.js index 1a84d0ea5..2fd6c6654 100644 --- a/Client/src/Utils/timeUtils.js +++ b/Client/src/Utils/timeUtils.js @@ -1,4 +1,5 @@ import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; import customParseFormat from "dayjs/plugin/customParseFormat"; @@ -6,6 +7,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat"; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(customParseFormat); +dayjs.extend(duration); export const MS_PER_SECOND = 1000; export const MS_PER_MINUTE = 60 * MS_PER_SECOND; @@ -75,6 +77,23 @@ export const formatDurationSplit = (ms) => { : { time: 0, format: "seconds" }; }; +export const getHumanReadableDuration = (ms) => { + const durationObj = dayjs.duration(ms); + if (durationObj.asDays() >= 1) { + const days = Math.floor(durationObj.asDays()); + return { time: days, units: days === 1 ? "day" : "days" }; + } else if (durationObj.asHours() >= 1) { + const hours = Math.floor(durationObj.asHours()); + return { time: hours, units: hours === 1 ? "hour" : "hours" }; + } else if (durationObj.asMinutes() >= 1) { + const minutes = Math.floor(durationObj.asMinutes()); + return { time: minutes, units: minutes === 1 ? "minute" : "minutes" }; + } else { + const seconds = Math.floor(durationObj.asSeconds()); + return { time: seconds, units: seconds === 1 ? "second" : "seconds" }; + } +}; + export const formatDate = (date, customOptions) => { const options = { year: "numeric", From 9381429f40daf50d59b586ee7a38fe2b5e7432a7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 13:17:23 -0800 Subject: [PATCH 62/76] Use Dot component in host --- .../Pages/Uptime/Home/Components/Host/index.jsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx index 91b7c418b..c2f082f3a 100644 --- a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx +++ b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx @@ -1,6 +1,7 @@ -import { Stack, Box, Typography } from "@mui/material"; +import { Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; +import Dot from "../../../../../Components/Dot"; /** * Host component. * This subcomponent receives a params object and displays the host details. @@ -26,16 +27,7 @@ const Host = ({ url, title, percentageColor, percentage }) => { {title} {percentageColor && percentage && ( <> - + Date: Mon, 27 Jan 2025 13:17:48 -0800 Subject: [PATCH 63/76] Consistent spacing in home page --- Client/src/Pages/Uptime/Home/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 2427d7e61..ec9e7d71c 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -102,7 +102,7 @@ const UptimeMonitors = () => { return ( From 671d297c2d5368f90cda43407941d2b1b1d73720 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 13:18:05 -0800 Subject: [PATCH 64/76] refactor details --- .../Components/ChartBoxes/index.jsx | 134 ++++++++++++++++++ .../NewDetails/Components/Charts/ChartBox.jsx | 74 ++++++++++ .../Components/Charts/CustomLabels.jsx | 42 ++++++ .../Components/Charts/DownBarChart.jsx | 92 ++++++++++++ .../Components/Charts/ResponseGaugeChart.jsx | 117 +++++++++++++++ .../Components/Charts/ResponseTimeChart.jsx | 18 +++ .../Components/Charts/UpBarChart.jsx | 109 ++++++++++++++ .../Components/ConfigButton/index.jsx | 35 +++++ .../Components/MonitorHeader/index.jsx | 43 ++++++ .../Components/ResponseTable/index.jsx | 0 .../Components/StatusBoxes/index.jsx | 72 ++++++++++ .../Components/TimeFramePicker/index.jsx | 44 ++++++ .../NewDetails/Hooks/useMonitorFetch.jsx | 2 +- Client/src/Pages/Uptime/NewDetails/index.jsx | 37 ++++- 14 files changed, 814 insertions(+), 5 deletions(-) create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/Charts/DownBarChart.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/ConfigButton/index.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/ResponseTable/index.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx create mode 100644 Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx diff --git a/Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx new file mode 100644 index 000000000..afe93d028 --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx @@ -0,0 +1,134 @@ +// Components +import { Stack, Typography, Box } from "@mui/material"; +import ChartBox from "../Charts/ChartBox"; +import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react"; +import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react"; +import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react"; +import UpBarChart from "../Charts/UpBarChart"; +import DownBarChart from "../Charts/DownBarChart"; +import ResponseGaugeChart from "../Charts/ResponseGaugeChart"; + +// Utils +import { formatDateWithTz } from "../../../../../Utils/timeUtils"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; + +const ChartBoxes = ({ + monitor, + dateRange, + uiTimezone, + dateFormat, + hoveredUptimeData, + setHoveredUptimeData, + hoveredIncidentsData, + setHoveredIncidentsData, +}) => { + const theme = useTheme(); + return ( + + } + header="Uptime" + > + + + Total Checks + + {hoveredUptimeData !== null + ? hoveredUptimeData.totalChecks + : (monitor?.groupedUpChecks?.reduce((count, checkGroup) => { + return count + checkGroup.totalChecks; + }, 0) ?? 0)} + + {hoveredUptimeData !== null && hoveredUptimeData.time !== null && ( + + {formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)} + + )} + + + + {hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"} + + + {hoveredUptimeData !== null + ? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0) + : Math.floor( + ((monitor?.upChecks?.totalChecks ?? 0) / + (monitor?.totalChecks ?? 1)) * + 100 + )} + + {hoveredUptimeData !== null ? " ms" : " %"} + + + + + + + } + header="Incidents" + > + + + {hoveredIncidentsData !== null + ? hoveredIncidentsData.totalChecks + : (monitor?.groupedDownChecks?.reduce((count, checkGroup) => { + return count + checkGroup.totalChecks; + }, 0) ?? 0)} + + {hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && ( + + {formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)} + + )} + + + + } + header="Average Response Time" + > + + + + ); +}; + +export default ChartBoxes; + +ChartBoxes.propTypes = { + monitor: PropTypes.object.isRequired, + dateRange: PropTypes.string.isRequired, + uiTimezone: PropTypes.string.isRequired, + dateFormat: PropTypes.string.isRequired, + hoveredUptimeData: PropTypes.object, + setHoveredUptimeData: PropTypes.func, + hoveredIncidentsData: PropTypes.object, + setHoveredIncidentsData: PropTypes.func, +}; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx new file mode 100644 index 000000000..e62949004 --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx @@ -0,0 +1,74 @@ +import { Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import IconBox from "../../../../../Components/IconBox"; +import PropTypes from "prop-types"; +const ChartBox = ({ children, icon, header }) => { + const theme = useTheme(); + return ( + span": { + color: theme.palette.primary.contrastText, + fontSize: 20, + "& span": { + opacity: 0.8, + marginLeft: 2, + fontSize: 15, + }, + }, + "& .MuiStack-root": { + flexDirection: "row", + gap: theme.spacing(6), + }, + "& .MuiStack-root:first-of-type": { + alignItems: "center", + }, + "& tspan, & text": { + fill: theme.palette.primary.contrastTextTertiary, + }, + "& path": { + transition: "fill 300ms ease, stroke-width 400ms ease", + }, + }} + > + + {icon} + {header} + + + {children} + + ); +}; + +export default ChartBox; + +ChartBox.propTypes = { + children: PropTypes.node, + icon: PropTypes.node.isRequired, + header: PropTypes.string.isRequired, +}; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx new file mode 100644 index 000000000..8df33953f --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx @@ -0,0 +1,42 @@ +import PropTypes from "prop-types"; +import { useSelector } from "react-redux"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils"; + +const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { + const uiTimezone = useSelector((state) => state.ui.timezone); + const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D"; + + return ( + <> + + {formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)} + + + {formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)} + + + ); +}; + +CustomLabels.propTypes = { + x: PropTypes.number.isRequired, + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + firstDataPoint: PropTypes.object, + lastDataPoint: PropTypes.object, + type: PropTypes.string.isRequired, +}; + +export default CustomLabels; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/DownBarChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/DownBarChart.jsx new file mode 100644 index 000000000..dcd3a06be --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/DownBarChart.jsx @@ -0,0 +1,92 @@ +import { memo, useState } from "react"; +import { useTheme } from "@mui/material"; +import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; +import PropTypes from "prop-types"; +import CustomLabels from "./CustomLabels"; + +const DownBarChart = memo(({ monitor, type, onBarHover }) => { + const theme = useTheme(); + + const [chartHovered, setChartHovered] = useState(false); + const [hoveredBarIndex, setHoveredBarIndex] = useState(null); + + return ( + + { + setChartHovered(true); + onBarHover({ time: null, totalChecks: 0 }); + }} + onMouseLeave={() => { + setChartHovered(false); + setHoveredBarIndex(null); + onBarHover(null); + }} + > + + } + /> + + {monitor?.groupedDownChecks?.map((entry, index) => { + return ( + { + setHoveredBarIndex(index); + onBarHover(entry); + }} + onMouseLeave={() => { + setHoveredBarIndex(null); + onBarHover({ time: null, totalChecks: 0 }); + }} + /> + ); + })} + + + + ); +}); + +DownBarChart.displayName = "DownBarChart"; +DownBarChart.propTypes = { + monitor: PropTypes.shape({ + groupedDownChecks: PropTypes.arrayOf(PropTypes.object), + }), + type: PropTypes.string, + onBarHover: PropTypes.func, +}; +export default DownBarChart; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx new file mode 100644 index 000000000..e94af8441 --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx @@ -0,0 +1,117 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; +import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts"; + +const ResponseGaugeChart = ({ avgResponseTime }) => { + const theme = useTheme(); + + let max = 1000; // max ms + + const data = [ + { response: max, fill: "transparent", background: false }, + { response: avgResponseTime, background: true }, + ]; + let responseTime = Math.floor(avgResponseTime); + let responseProps = + responseTime <= 200 + ? { + category: "Excellent", + main: theme.palette.success.main, + bg: theme.palette.success.contrastText, + } + : responseTime <= 500 + ? { + category: "Fair", + main: theme.palette.success.main, + bg: theme.palette.success.contrastText, + } + : responseTime <= 600 + ? { + category: "Acceptable", + main: theme.palette.warning.main, + bg: theme.palette.warning.lowContrast, + } + : { + category: "Poor", + main: theme.palette.error.main, + bg: theme.palette.error.contrastText, + }; + + return ( + + + + low + + + high + + + {responseProps.category} + + + {responseTime} ms + + + + + + + + ); +}; + +ResponseGaugeChart.propTypes = { + avgResponseTime: PropTypes.number.isRequired, +}; + +export default ResponseGaugeChart; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx new file mode 100644 index 000000000..c28163394 --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx @@ -0,0 +1,18 @@ +import ChartBox from "./ChartBox"; +import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart"; +import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react"; +const ResponseTImeChart = ({ monitor, dateRange }) => { + return ( + } + header="Response Times" + > + + + ); +}; + +export default ResponseTImeChart; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx new file mode 100644 index 000000000..1df79c26e --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx @@ -0,0 +1,109 @@ +import { memo, useState } from "react"; +import { useTheme } from "@mui/material"; +import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; +import PropTypes from "prop-types"; +import CustomLabels from "./CustomLabels"; + +const getThemeColor = (responseTime) => { + if (responseTime < 200) { + return "success"; + } else if (responseTime < 300) { + return "warning"; + } else { + return "error"; + } +}; + +const UpBarChart = memo(({ monitor, type, onBarHover }) => { + const theme = useTheme(); + const [chartHovered, setChartHovered] = useState(false); + const [hoveredBarIndex, setHoveredBarIndex] = useState(null); + + return ( + + { + setChartHovered(true); + onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 }); + }} + onMouseLeave={() => { + setChartHovered(false); + setHoveredBarIndex(null); + onBarHover(null); + }} + > + + } + /> + + {monitor?.groupedUpChecks?.map((entry, index) => { + const themeColor = getThemeColor(entry.avgResponseTime); + return ( + { + setHoveredBarIndex(index); + onBarHover(entry); + }} + onMouseLeave={() => { + setHoveredBarIndex(null); + onBarHover({ + time: null, + totalChecks: 0, + groupUptimePercentage: 0, + }); + }} + /> + ); + })} + + + + ); +}); + +// Add display name for the component +UpBarChart.displayName = "UpBarChart"; + +// Validate props using PropTypes +UpBarChart.propTypes = { + monitor: PropTypes.shape({ + groupedUpChecks: PropTypes.array, + }), + type: PropTypes.string, + onBarHover: PropTypes.func, +}; +export default UpBarChart; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/ConfigButton/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/ConfigButton/index.jsx new file mode 100644 index 000000000..9981b4e1c --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/ConfigButton/index.jsx @@ -0,0 +1,35 @@ +import { Button, Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; +import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react"; + +const ConfigButton = ({ shouldRender, monitorId }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + if (!shouldRender) return null; + + return ( + + + + ); +}; + +export default ConfigButton; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx new file mode 100644 index 000000000..5802fd82d --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx @@ -0,0 +1,43 @@ +import { Stack, Typography } from "@mui/material"; +import PulseDot from "../../../../../Components/Animated/PulseDot"; +import Dot from "../../../../../Components/Dot"; +import { useTheme } from "@emotion/react"; +import useUtils from "../../../Home/Hooks/useUtils"; +import { formatDurationRounded } from "../../../../../Utils/timeUtils"; +import ConfigButton from "../ConfigButton"; + +const MonitorHeader = ({ monitor }) => { + const theme = useTheme(); + const { statusColor, statusMsg, determineState } = useUtils(); + + return ( + + + {monitor.name} + + + + {monitor?.url?.replace(/^https?:\/\//, "") || "..."} + + + + Checking every {formatDurationRounded(monitor?.interval)}. + + + + + + ); +}; + +export default MonitorHeader; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/ResponseTable/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/ResponseTable/index.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..e994dd63b --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx @@ -0,0 +1,72 @@ +// Components +import { Stack, Typography } from "@mui/material"; +import StatBox from "../../../../../Components/StatBox"; + +// Utils +import { useTheme } from "@mui/material/styles"; +import useUtils from "../../../Home/Hooks/useUtils"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; + +const StatusBoxes = ({ monitor, certificateExpiry }) => { + const theme = useTheme(); + const { time: streakTime, units: streakUnits } = getHumanReadableDuration( + monitor?.uptimeStreak + ); + + const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration( + monitor?.timeSinceLastCheck + ); + + const { determineState } = useUtils(); + return ( + + + {streakTime} + {streakUnits} + + } + /> + + {lastCheckTime} + {lastCheckUnits} + {"ago"} + + } + /> + + {monitor?.latestResponseTime} + {"ms"} + + } + /> + + {certificateExpiry} + + } + /> + + ); +}; + +export default StatusBoxes; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx new file mode 100644 index 000000000..ffb581e23 --- /dev/null +++ b/Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx @@ -0,0 +1,44 @@ +import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; +import { useTheme } from "@emotion/react"; +const TimeFramePicker = ({ dateRange, setDateRange }) => { + const theme = useTheme(); + return ( + + + Showing statistics for past{" "} + {dateRange === "day" ? "24 hours" : dateRange === "week" ? "7 days" : "30 days"}. + + + + + + + + ); +}; + +export default TimeFramePicker; diff --git a/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx index e7ed825f2..067545f51 100644 --- a/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx +++ b/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx @@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom"; export const useMonitorFetch = ({ authToken, monitorId, dateRange }) => { const [monitorIsLoading, setMonitorsIsLoading] = useState(false); - const [monitor, setMonitor] = useState([]); + const [monitor, setMonitor] = useState({}); const navigate = useNavigate(); useEffect(() => { diff --git a/Client/src/Pages/Uptime/NewDetails/index.jsx b/Client/src/Pages/Uptime/NewDetails/index.jsx index 5494146e6..eca12ec57 100644 --- a/Client/src/Pages/Uptime/NewDetails/index.jsx +++ b/Client/src/Pages/Uptime/NewDetails/index.jsx @@ -1,6 +1,10 @@ // Components import Breadcrumbs from "../../../Components/Breadcrumbs"; - +import MonitorHeader from "./Components/MonitorHeader"; +import StatusBoxes from "./Components/StatusBoxes"; +import TimeFramePicker from "./Components/TimeFramePicker"; +import ChartBoxes from "./Components/ChartBoxes"; +import ResponseTimeChart from "./Components/Charts/ResponseTimeChart"; // MUI Components import { Stack } from "@mui/material"; @@ -8,6 +12,7 @@ import { Stack } from "@mui/material"; import { useState } from "react"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; +import { useTheme } from "@emotion/react"; import useMonitorFetch from "./Hooks/useMonitorFetch"; import useCertificateFetch from "./Hooks/useCertificateFetch"; // Constants @@ -26,10 +31,13 @@ const UptimeDetails = () => { // Local state const [dateRange, setDateRange] = useState("day"); + const [hoveredUptimeData, setHoveredUptimeData] = useState(null); + const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); // Utils const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; const { monitorId } = useParams(); + const theme = useTheme(); const { monitor, monitorIsLoading } = useMonitorFetch({ authToken, @@ -45,11 +53,32 @@ const UptimeDetails = () => { uiTimezone, }); - console.log(monitor); - console.log(certificateExpiry); return ( - + + + + + + ); }; From 36d46c246e093771efac1bdcabf3ecbb0bedbab1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 14:55:44 -0800 Subject: [PATCH 65/76] Add pagination label to prop types --- Client/src/Components/Table/TablePagination/index.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/src/Components/Table/TablePagination/index.jsx b/Client/src/Components/Table/TablePagination/index.jsx index 61e89232f..518c18599 100644 --- a/Client/src/Components/Table/TablePagination/index.jsx +++ b/Client/src/Components/Table/TablePagination/index.jsx @@ -5,6 +5,7 @@ import { TablePaginationActions } from "./Actions"; import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react"; Pagination.propTypes = { + paginationLabel: PropTypes.string, // Label for the pagination. itemCount: PropTypes.number.isRequired, // Total number of items for pagination. page: PropTypes.number.isRequired, // Current page index. rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page. From cb0ec0e1826462303d891ce6e302b56b74efbadb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Jan 2025 14:55:55 -0800 Subject: [PATCH 66/76] finish refactor --- .../Uptime/Details/Charts/CustomLabels.jsx | 42 -- .../Uptime/Details/Charts/DownBarChart.jsx | 92 --- .../Uptime/Details/Charts/UpBarChart.jsx | 107 ---- .../Components/ChartBoxes/index.jsx | 8 +- .../Components/ChartBoxes/skeleton.jsx | 30 + .../Components/Charts/ChartBox.jsx | 4 +- .../Components/Charts/CustomLabels.jsx | 0 .../Components/Charts/DownBarChart.jsx | 0 .../Charts/ResponseGaugeChart.jsx | 0 .../Components/Charts/ResponseTimeChart.jsx | 7 +- .../Charts/ResponseTimeChartSkeleton.jsx | 12 + .../Components/Charts/UpBarChart.jsx | 0 .../Components/ConfigButton/index.jsx | 0 .../Components/MonitorHeader/index.jsx | 16 +- .../Components/MonitorHeader/skeleton.jsx | 23 + .../Components/ResponseTable/index.jsx | 88 +++ .../Components/ResponseTable/skeleton.jsx | 13 + .../Components/StatusBoxes/index.jsx | 8 +- .../Components/StatusBoxes/skeleton.jsx | 30 + .../Components/TimeFramePicker/index.jsx | 9 +- .../Components/TimeFramePicker/skeleton.jsx | 23 + .../Hooks/useCertificateFetch.jsx | 0 .../Uptime/Details/Hooks/useChecksFetch.jsx | 45 ++ .../Hooks/useMonitorFetch.jsx | 0 .../Uptime/Details/PaginationTable/index.jsx | 163 ----- Client/src/Pages/Uptime/Details/index.css | 0 Client/src/Pages/Uptime/Details/index.jsx | 582 ++++-------------- Client/src/Pages/Uptime/Details/skeleton.jsx | 120 ---- Client/src/Pages/Uptime/Details/styled.jsx | 45 -- .../Components/Charts/ResponseGaugeChart.jsx | 117 ---- .../Components/ResponseTable/index.jsx | 0 Client/src/Pages/Uptime/NewDetails/index.jsx | 86 --- Client/src/Routes/index.jsx | 9 +- 33 files changed, 428 insertions(+), 1251 deletions(-) delete mode 100644 Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx delete mode 100644 Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx delete mode 100644 Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/ChartBoxes/index.jsx (97%) create mode 100644 Client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/Charts/ChartBox.jsx (95%) rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/Charts/CustomLabels.jsx (100%) rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/Charts/DownBarChart.jsx (100%) rename Client/src/Pages/Uptime/Details/{ => Components}/Charts/ResponseGaugeChart.jsx (100%) rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/Charts/ResponseTimeChart.jsx (70%) create mode 100644 Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/Charts/UpBarChart.jsx (100%) rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/ConfigButton/index.jsx (100%) rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/MonitorHeader/index.jsx (75%) create mode 100644 Client/src/Pages/Uptime/Details/Components/MonitorHeader/skeleton.jsx create mode 100644 Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx create mode 100644 Client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/StatusBoxes/index.jsx (90%) create mode 100644 Client/src/Pages/Uptime/Details/Components/StatusBoxes/skeleton.jsx rename Client/src/Pages/Uptime/{NewDetails => Details}/Components/TimeFramePicker/index.jsx (85%) create mode 100644 Client/src/Pages/Uptime/Details/Components/TimeFramePicker/skeleton.jsx rename Client/src/Pages/Uptime/{NewDetails => Details}/Hooks/useCertificateFetch.jsx (100%) create mode 100644 Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx rename Client/src/Pages/Uptime/{NewDetails => Details}/Hooks/useMonitorFetch.jsx (100%) delete mode 100644 Client/src/Pages/Uptime/Details/PaginationTable/index.jsx delete mode 100644 Client/src/Pages/Uptime/Details/index.css delete mode 100644 Client/src/Pages/Uptime/Details/skeleton.jsx delete mode 100644 Client/src/Pages/Uptime/Details/styled.jsx delete mode 100644 Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx delete mode 100644 Client/src/Pages/Uptime/NewDetails/Components/ResponseTable/index.jsx delete mode 100644 Client/src/Pages/Uptime/NewDetails/index.jsx diff --git a/Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx b/Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx deleted file mode 100644 index 10494830f..000000000 --- a/Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../Utils/timeUtils"; - -const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D"; - - return ( - <> - - {formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)} - - - {formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)} - - - ); -}; - -CustomLabels.propTypes = { - x: PropTypes.number.isRequired, - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - firstDataPoint: PropTypes.object.isRequired, - lastDataPoint: PropTypes.object.isRequired, - type: PropTypes.string.isRequired, -}; - -export default CustomLabels; diff --git a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx b/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx deleted file mode 100644 index ae2adda0f..000000000 --- a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx +++ /dev/null @@ -1,92 +0,0 @@ -import { memo, useState } from "react"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; -import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels"; - -const DownBarChart = memo(({ monitor, type, onBarHover }) => { - const theme = useTheme(); - - const [chartHovered, setChartHovered] = useState(false); - const [hoveredBarIndex, setHoveredBarIndex] = useState(null); - - return ( - - { - setChartHovered(true); - onBarHover({ time: null, totalChecks: 0 }); - }} - onMouseLeave={() => { - setChartHovered(false); - setHoveredBarIndex(null); - onBarHover(null); - }} - > - - } - /> - - {monitor.groupedDownChecks.map((entry, index) => { - return ( - { - setHoveredBarIndex(index); - onBarHover(entry); - }} - onMouseLeave={() => { - setHoveredBarIndex(null); - onBarHover({ time: null, totalChecks: 0 }); - }} - /> - ); - })} - - - - ); -}); - -DownBarChart.displayName = "DownBarChart"; -DownBarChart.propTypes = { - monitor: PropTypes.shape({ - groupedDownChecks: PropTypes.arrayOf(PropTypes.object), - }), - type: PropTypes.string, - onBarHover: PropTypes.func, -}; -export default DownBarChart; diff --git a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx b/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx deleted file mode 100644 index 223dc8eef..000000000 --- a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { memo, useState } from "react"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; -import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels"; - -const getThemeColor = (responseTime) => { - if (responseTime < 200) { - return "success"; - } else if (responseTime < 300) { - return "warning"; - } else { - return "error"; - } -}; - -const UpBarChart = memo(({ monitor, type, onBarHover }) => { - const theme = useTheme(); - const [chartHovered, setChartHovered] = useState(false); - const [hoveredBarIndex, setHoveredBarIndex] = useState(null); - - return ( - - { - setChartHovered(true); - onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 }); - }} - onMouseLeave={() => { - setChartHovered(false); - setHoveredBarIndex(null); - onBarHover(null); - }} - > - - } - /> - - {monitor.groupedUpChecks.map((entry, index) => { - const themeColor = getThemeColor(entry.avgResponseTime); - return ( - { - setHoveredBarIndex(index); - onBarHover(entry); - }} - onMouseLeave={() => { - setHoveredBarIndex(null); - onBarHover({ - time: null, - totalChecks: 0, - groupUptimePercentage: 0, - }); - }} - /> - ); - })} - - - - ); -}); - -// Add display name for the component -UpBarChart.displayName = "UpBarChart"; - -// Validate props using PropTypes -UpBarChart.propTypes = { - monitor: PropTypes.shape({ - groupedUpChecks: PropTypes.array, - }), - type: PropTypes.string, - onBarHover: PropTypes.func, -}; -export default UpBarChart; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx similarity index 97% rename from Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx rename to Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx index afe93d028..d9e581267 100644 --- a/Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -7,13 +7,14 @@ import AverageResponseIcon from "../../../../../assets/icons/average-response-ic import UpBarChart from "../Charts/UpBarChart"; import DownBarChart from "../Charts/DownBarChart"; import ResponseGaugeChart from "../Charts/ResponseGaugeChart"; - +import SkeletonLayout from "./skeleton"; // Utils import { formatDateWithTz } from "../../../../../Utils/timeUtils"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; const ChartBoxes = ({ + shouldRender = true, monitor, dateRange, uiTimezone, @@ -24,6 +25,11 @@ const ChartBoxes = ({ setHoveredIncidentsData, }) => { const theme = useTheme(); + + if (!shouldRender) { + return ; + } + return ( { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx similarity index 95% rename from Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx index e62949004..672134781 100644 --- a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx +++ b/Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx @@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import IconBox from "../../../../../Components/IconBox"; import PropTypes from "prop-types"; -const ChartBox = ({ children, icon, header }) => { +const ChartBox = ({ children, icon, header, height = "300px" }) => { const theme = useTheme(); return ( { justifyContent: "space-between", flex: "1 30%", gap: theme.spacing(8), - height: 300, + height, minWidth: 250, padding: theme.spacing(8), border: 1, diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx similarity index 100% rename from Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/DownBarChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx similarity index 100% rename from Client/src/Pages/Uptime/NewDetails/Components/Charts/DownBarChart.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx diff --git a/Client/src/Pages/Uptime/Details/Charts/ResponseGaugeChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx similarity index 100% rename from Client/src/Pages/Uptime/Details/Charts/ResponseGaugeChart.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx similarity index 70% rename from Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index c28163394..73ffc035e 100644 --- a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx +++ b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -1,7 +1,12 @@ import ChartBox from "./ChartBox"; import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart"; import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react"; -const ResponseTImeChart = ({ monitor, dateRange }) => { +import SkeletonLayout from "./ResponseTimeChartSkeleton"; +const ResponseTImeChart = ({ shouldRender = true, monitor, dateRange }) => { + if (!shouldRender) { + return ; + } + return ( } diff --git a/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx new file mode 100644 index 000000000..a70757e36 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx @@ -0,0 +1,12 @@ +import { Skeleton } from "@mui/material"; +const ResponseTimeChartSkeleton = () => { + return ( + + ); +}; + +export default ResponseTimeChartSkeleton; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx similarity index 100% rename from Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx diff --git a/Client/src/Pages/Uptime/NewDetails/Components/ConfigButton/index.jsx b/Client/src/Pages/Uptime/Details/Components/ConfigButton/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/NewDetails/Components/ConfigButton/index.jsx rename to Client/src/Pages/Uptime/Details/Components/ConfigButton/index.jsx diff --git a/Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx similarity index 75% rename from Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx rename to Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx index 5802fd82d..fddf9c3e5 100644 --- a/Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx @@ -5,10 +5,16 @@ import { useTheme } from "@emotion/react"; import useUtils from "../../../Home/Hooks/useUtils"; import { formatDurationRounded } from "../../../../../Utils/timeUtils"; import ConfigButton from "../ConfigButton"; +import SkeletonLayout from "./skeleton"; +import PropTypes from "prop-types"; -const MonitorHeader = ({ monitor }) => { +const MonitorHeader = ({ shouldRender = true, isAdmin, monitor }) => { const theme = useTheme(); const { statusColor, statusMsg, determineState } = useUtils(); + console.log(shouldRender); + if (!shouldRender) { + return ; + } return ( { ); }; +MonitorHeader.propTypes = { + shouldRender: PropTypes.bool, + isAdmin: PropTypes.bool, + monitor: PropTypes.object, +}; + export default MonitorHeader; diff --git a/Client/src/Pages/Uptime/Details/Components/MonitorHeader/skeleton.jsx b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/skeleton.jsx new file mode 100644 index 000000000..64dc0547f --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/skeleton.jsx @@ -0,0 +1,23 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx new file mode 100644 index 000000000..a510b9bf1 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx @@ -0,0 +1,88 @@ +import ChartBox from "../Charts/ChartBox"; +import PropTypes from "prop-types"; +import HistoryIcon from "../../../../../assets/icons/history-icon.svg?react"; +import Table from "../../../../../Components/Table"; +import TablePagination from "../../../../../Components/Table/TablePagination"; +import { StatusLabel } from "../../../../../Components/Label"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils"; +import SkeletonLayout from "./skeleton"; +const ResponseTable = ({ + shouldRender = true, + checks, + checksCount, + uiTimezone, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}) => { + if (!shouldRender) { + return ; + } + + const headers = [ + { + id: "status", + content: "Status", + render: (row) => { + const status = row.status === true ? "up" : "down"; + + return ( + + ); + }, + }, + { + id: "date", + content: "Date & Time", + render: (row) => + formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone), + }, + { + id: "statusCode", + content: "Status code", + render: (row) => (row.statusCode ? row.statusCode : "N/A"), + }, + { + id: "message", + content: "Message", + render: (row) => row.message, + }, + ]; + + return ( + } + header="Response Times" + height="100%" + > + + + + ); +}; + +ResponseTable.propTypes = { + checks: PropTypes.array.isRequired, + checksCount: PropTypes.number.isRequired, + uiTimezone: PropTypes.string.isRequired, + page: PropTypes.number.isRequired, + setPage: PropTypes.func.isRequired, + rowsPerPage: PropTypes.number.isRequired, + setRowsPerPage: PropTypes.func.isRequired, +}; + +export default ResponseTable; diff --git a/Client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx b/Client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx new file mode 100644 index 000000000..2665ffaf1 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx @@ -0,0 +1,13 @@ +import { Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx similarity index 90% rename from Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx rename to Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx index e994dd63b..55bcf34da 100644 --- a/Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx @@ -1,13 +1,16 @@ // Components import { Stack, Typography } from "@mui/material"; import StatBox from "../../../../../Components/StatBox"; - +import SkeletonLayout from "./skeleton"; // Utils import { useTheme } from "@mui/material/styles"; import useUtils from "../../../Home/Hooks/useUtils"; import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; -const StatusBoxes = ({ monitor, certificateExpiry }) => { +const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => { + if (!shouldRender) { + return ; + } const theme = useTheme(); const { time: streakTime, units: streakUnits } = getHumanReadableDuration( monitor?.uptimeStreak @@ -18,6 +21,7 @@ const StatusBoxes = ({ monitor, certificateExpiry }) => { ); const { determineState } = useUtils(); + return ( { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx b/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx similarity index 85% rename from Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx rename to Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx index ffb581e23..ade677604 100644 --- a/Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx @@ -1,7 +1,14 @@ import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; import { useTheme } from "@emotion/react"; -const TimeFramePicker = ({ dateRange, setDateRange }) => { +import SkeletonLayout from "./skeleton"; + +const TimeFramePicker = ({ shouldRender = true, dateRange, setDateRange }) => { const theme = useTheme(); + + if (!shouldRender) { + return ; + } + return ( { + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx similarity index 100% rename from Client/src/Pages/Uptime/NewDetails/Hooks/useCertificateFetch.jsx rename to Client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx diff --git a/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx new file mode 100644 index 000000000..2a50c2172 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { useEffect } from "react"; +import { logger } from "../../../../Utils/Logger"; +import { networkService } from "../../../../main"; + +export const useChecksFetch = ({ + authToken, + monitorId, + dateRange, + page, + rowsPerPage, +}) => { + const [checks, setChecks] = useState([]); + const [checksCount, setChecksCount] = useState(0); + const [checksAreLoading, setChecksAreLoading] = useState(false); + + useEffect(() => { + const fetchChecks = async () => { + try { + setChecksAreLoading(true); + const res = await networkService.getChecksByMonitor({ + authToken: authToken, + monitorId: monitorId, + sortOrder: "desc", + limit: null, + dateRange: dateRange, + filter: null, + page: page, + rowsPerPage: rowsPerPage, + }); + setChecks(res.data.data.checks); + setChecksCount(res.data.data.checksCount); + } catch (error) { + logger.error(error); + } finally { + setChecksAreLoading(false); + } + }; + fetchChecks(); + }, [authToken, monitorId, dateRange, page, rowsPerPage]); + + return { checks, checksCount, checksAreLoading }; +}; + +export default useChecksFetch; diff --git a/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx similarity index 100% rename from Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx rename to Client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx diff --git a/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx b/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx deleted file mode 100644 index 93e9532c5..000000000 --- a/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx +++ /dev/null @@ -1,163 +0,0 @@ -import PropTypes from "prop-types"; -import { - TableContainer, - Table, - TableHead, - TableRow, - TableCell, - TableBody, - PaginationItem, - Pagination, - Paper, -} from "@mui/material"; - -import { useState, useEffect } from "react"; -import { useSelector } from "react-redux"; -import { networkService } from "../../../../main"; -import { StatusLabel } from "../../../../Components/Label"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded"; -import { logger } from "../../../../Utils/Logger"; -import { formatDateWithTz } from "../../../../Utils/timeUtils"; -import { useTheme } from "@emotion/react"; -const PaginationTable = ({ monitorId, dateRange }) => { - const theme = useTheme(); - const { authToken } = useSelector((state) => state.auth); - const [checks, setChecks] = useState([]); - const [checksCount, setChecksCount] = useState(0); - const [paginationController, setPaginationController] = useState({ - page: 0, - rowsPerPage: 5, - }); - const uiTimezone = useSelector((state) => state.ui.timezone); - - useEffect(() => { - setPaginationController((prevPaginationController) => ({ - ...prevPaginationController, - page: 0, - })); - }, [dateRange]); - - useEffect(() => { - const fetchPage = async () => { - try { - const res = await networkService.getChecksByMonitor({ - authToken: authToken, - monitorId: monitorId, - sortOrder: "desc", - limit: null, - dateRange: dateRange, - filter: null, - page: paginationController.page, - rowsPerPage: paginationController.rowsPerPage, - }); - setChecks(res.data.data.checks); - setChecksCount(res.data.data.checksCount); - } catch (error) { - logger.error(error); - } - }; - fetchPage(); - }, [ - authToken, - monitorId, - dateRange, - paginationController.page, - paginationController.rowsPerPage, - ]); - - const handlePageChange = (_, newPage) => { - setPaginationController({ - ...paginationController, - page: newPage - 1, // 0-indexed - }); - }; - - let paginationComponent = <>; - if (checksCount > paginationController.rowsPerPage) { - paginationComponent = ( - ( - - )} - /> - ); - } - - return ( - <> - -
- - - Status - Date & Time - Status Code - Message - - - - {checks.map((check) => { - const status = check.status === true ? "up" : "down"; - - return ( - - - - - - {formatDateWithTz( - check.createdAt, - "ddd, MMMM D, YYYY, HH:mm A", - uiTimezone - )} - - {check.statusCode ? check.statusCode : "N/A"} - {check.message} - - ); - })} - -
- - {paginationComponent} - - ); -}; - -PaginationTable.propTypes = { - monitorId: PropTypes.string.isRequired, - dateRange: PropTypes.string.isRequired, -}; - -export default PaginationTable; diff --git a/Client/src/Pages/Uptime/Details/index.css b/Client/src/Pages/Uptime/Details/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx index be589c1ba..81265eb56 100644 --- a/Client/src/Pages/Uptime/Details/index.jsx +++ b/Client/src/Pages/Uptime/Details/index.jsx @@ -1,476 +1,128 @@ -import PropTypes from "prop-types"; -import { useEffect, useState, useCallback } from "react"; -import { Box, Button, Stack, Tooltip, Typography, useTheme } from "@mui/material"; -import { useSelector } from "react-redux"; -import { useNavigate, useParams } from "react-router-dom"; -import { networkService } from "../../../main"; -import { logger } from "../../../Utils/Logger"; -import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart"; -import ButtonGroup from "@mui/material/ButtonGroup"; -import SettingsIcon from "../../../assets/icons/settings-bold.svg?react"; -import UptimeIcon from "../../../assets/icons/uptime-icon.svg?react"; -import ResponseTimeIcon from "../../../assets/icons/response-time-icon.svg?react"; -import AverageResponseIcon from "../../../assets/icons/average-response-icon.svg?react"; -import IncidentsIcon from "../../../assets/icons/incidents.svg?react"; -import HistoryIcon from "../../../assets/icons/history-icon.svg?react"; -import PaginationTable from "./PaginationTable"; +// Components import Breadcrumbs from "../../../Components/Breadcrumbs"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import { ChartBox } from "./styled"; -import SkeletonLayout from "./skeleton"; -import "./index.css"; -import useUtils from "../Home/Hooks/useUtils"; -import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import IconBox from "../../../Components/IconBox"; -import StatBox from "../../../Components/StatBox"; -import UpBarChart from "./Charts/UpBarChart"; -import DownBarChart from "./Charts/DownBarChart"; -import ResponseGaugeChart from "./Charts/ResponseGaugeChart"; -/** - * Details page component displaying monitor details and related information. - * @component - */ -const DetailsPage = () => { - const theme = useTheme(); - const { statusColor, statusMsg, determineState } = useUtils(); - const isAdmin = useIsAdmin(); - const [monitor, setMonitor] = useState({}); - const { monitorId } = useParams(); - const { authToken } = useSelector((state) => state.auth); - const [dateRange, setDateRange] = useState("day"); - const [certificateExpiry, setCertificateExpiry] = useState("N/A"); - const navigate = useNavigate(); +import MonitorHeader from "./Components/MonitorHeader"; +import StatusBoxes from "./Components/StatusBoxes"; +import TimeFramePicker from "./Components/TimeFramePicker"; +import ChartBoxes from "./Components/ChartBoxes"; +import ResponseTimeChart from "./Components/Charts/ResponseTimeChart"; +import ResponseTable from "./Components/ResponseTable"; +// MUI Components +import { Stack } from "@mui/material"; - const certificateDateFormat = "MMM D, YYYY h A"; - const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; +// Utils +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useTheme } from "@emotion/react"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import useMonitorFetch from "./Hooks/useMonitorFetch"; +import useCertificateFetch from "./Hooks/useCertificateFetch"; +import useChecksFetch from "./Hooks/useChecksFetch"; + +// Constants +const BREADCRUMBS = [ + { name: "uptime", path: "/uptime" }, + { name: "details", path: "" }, + // { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy +]; + +const certificateDateFormat = "MMM D, YYYY h A"; + +const UptimeDetails = () => { + // Redux state + const { authToken } = useSelector((state) => state.auth); const uiTimezone = useSelector((state) => state.ui.timezone); - const fetchMonitor = useCallback(async () => { - try { - const res = await networkService.getUptimeDetailsById({ - authToken: authToken, - monitorId: monitorId, - dateRange: dateRange, - normalize: true, - }); - setMonitor(res?.data?.data ?? {}); - } catch (error) { - logger.error(error); - navigate("/not-found", { replace: true }); - } - }, [authToken, monitorId, navigate, dateRange]); - - useEffect(() => { - fetchMonitor(); - }, [fetchMonitor]); - - useEffect(() => { - const fetchCertificate = async () => { - if (monitor?.type !== "http") { - return; - } - try { - const res = await networkService.getCertificateExpiry({ - authToken: authToken, - monitorId: monitorId, - }); - if (res?.data?.data?.certificateDate) { - const date = res.data.data.certificateDate; - setCertificateExpiry( - formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A" - ); - } - } catch (error) { - setCertificateExpiry("N/A"); - console.error(error); - } - }; - fetchCertificate(); - }, [authToken, monitorId, monitor, uiTimezone, dateFormat]); - - const splitDuration = (duration) => { - const { time, format } = formatDurationSplit(duration); - return ( - <> - {time} - {format} - - ); - }; - - let loading = Object.keys(monitor).length === 0; - + // Local state + const [dateRange, setDateRange] = useState("day"); const [hoveredUptimeData, setHoveredUptimeData] = useState(null); const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + // Utils + const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; + const { monitorId } = useParams(); + const theme = useTheme(); + const isAdmin = useIsAdmin(); + + const { monitor, monitorIsLoading } = useMonitorFetch({ + authToken, + monitorId, + dateRange, + }); + + const { certificateExpiry, certificateIsLoading } = useCertificateFetch({ + monitor, + authToken, + monitorId, + certificateDateFormat, + uiTimezone, + }); + + const { checks, checksCount, checksAreLoading } = useChecksFetch({ + authToken, + monitorId, + dateRange, + page, + rowsPerPage, + }); + + // Handlers + const handlePageChange = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); + }; - const BREADCRUMBS = [ - { name: "uptime", path: "/uptime" }, - { name: "details", path: `/uptime/${monitorId}` }, - ]; return ( - - {loading ? ( - - ) : ( - <> - - - - - - {monitor.name} - - - {/* TODO there is a tooltip at BarChart component. Wrap the Tooltip on our own component */} - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {/* Checking every {formatDurationRounded(monitor?.interval)}. */} - - - - - {isAdmin && ( - - )} - - - - - - - {monitor?.latestResponseTime} - {"ms"} - - } - /> - - {certificateExpiry} - - } - /> - - - - - Showing statistics for past{" "} - {dateRange === "day" - ? "24 hours" - : dateRange === "week" - ? "7 days" - : "30 days"} - . - - - - - - - - - - - - - - Uptime - - - - Total Checks - - {hoveredUptimeData !== null - ? hoveredUptimeData.totalChecks - : (monitor?.groupedUpChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredUptimeData !== null && hoveredUptimeData.time !== null && ( - - {formatDateWithTz( - hoveredUptimeData._id, - dateFormat, - uiTimezone - )} - - )} - - - - {hoveredUptimeData !== null - ? "Avg Response Time" - : "Uptime Percentage"} - - - {hoveredUptimeData !== null - ? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0) - : Math.floor( - ((monitor?.upChecks?.totalChecks ?? 0) / - (monitor?.totalChecks ?? 1)) * - 100 - )} - - {hoveredUptimeData !== null ? " ms" : " %"} - - - - - - - - - - - - Incidents - - - Total Incidents - - {hoveredIncidentsData !== null - ? hoveredIncidentsData.totalChecks - : (monitor?.groupedDownChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredIncidentsData !== null && - hoveredIncidentsData.time !== null && ( - - {formatDateWithTz( - hoveredIncidentsData._id, - dateFormat, - uiTimezone - )} - - )} - - - - - - - - - Average Response Time - - - - - - - - - Response Times - - - - - - - - - - History - - - - - - - - - )} - + + + + + + + + + ); }; -DetailsPage.propTypes = { - isAdmin: PropTypes.bool, -}; -export default DetailsPage; +export default UptimeDetails; diff --git a/Client/src/Pages/Uptime/Details/skeleton.jsx b/Client/src/Pages/Uptime/Details/skeleton.jsx deleted file mode 100644 index 0bdc03ca8..000000000 --- a/Client/src/Pages/Uptime/Details/skeleton.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Box, Skeleton, Stack, useTheme } from "@mui/material"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Details/styled.jsx b/Client/src/Pages/Uptime/Details/styled.jsx deleted file mode 100644 index 164fee457..000000000 --- a/Client/src/Pages/Uptime/Details/styled.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Stack, styled } from "@mui/material"; - -export const ChartBox = styled(Stack)(({ theme }) => ({ - flex: "1 30%", - gap: theme.spacing(8), - height: 300, - minWidth: 250, - padding: theme.spacing(8), - border: 1, - borderStyle: "solid", - borderColor: theme.palette.primary.lowContrast, - borderRadius: 4, - backgroundColor: theme.palette.primary.main, - "& h2": { - color: theme.palette.primary.contrastTextSecondary, - fontSize: 15, - fontWeight: 500, - }, - "& .MuiBox-root:not(.area-tooltip) p": { - color: theme.palette.primary.contrastTextTertiary, - fontSize: 13, - }, - "& .MuiBox-root > span": { - color: theme.palette.primary.contrastText, - fontSize: 20, - "& span": { - opacity: 0.8, - marginLeft: 2, - fontSize: 15, - }, - }, - "& .MuiStack-root": { - flexDirection: "row", - gap: theme.spacing(6), - }, - "& .MuiStack-root:first-of-type": { - alignItems: "center", - }, - "& tspan, & text": { - fill: theme.palette.primary.contrastTextTertiary, - }, - "& path": { - transition: "fill 300ms ease, stroke-width 400ms ease", - }, -})); diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx deleted file mode 100644 index e94af8441..000000000 --- a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts"; - -const ResponseGaugeChart = ({ avgResponseTime }) => { - const theme = useTheme(); - - let max = 1000; // max ms - - const data = [ - { response: max, fill: "transparent", background: false }, - { response: avgResponseTime, background: true }, - ]; - let responseTime = Math.floor(avgResponseTime); - let responseProps = - responseTime <= 200 - ? { - category: "Excellent", - main: theme.palette.success.main, - bg: theme.palette.success.contrastText, - } - : responseTime <= 500 - ? { - category: "Fair", - main: theme.palette.success.main, - bg: theme.palette.success.contrastText, - } - : responseTime <= 600 - ? { - category: "Acceptable", - main: theme.palette.warning.main, - bg: theme.palette.warning.lowContrast, - } - : { - category: "Poor", - main: theme.palette.error.main, - bg: theme.palette.error.contrastText, - }; - - return ( - - - - low - - - high - - - {responseProps.category} - - - {responseTime} ms - - - - - - - - ); -}; - -ResponseGaugeChart.propTypes = { - avgResponseTime: PropTypes.number.isRequired, -}; - -export default ResponseGaugeChart; diff --git a/Client/src/Pages/Uptime/NewDetails/Components/ResponseTable/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/ResponseTable/index.jsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/Client/src/Pages/Uptime/NewDetails/index.jsx b/Client/src/Pages/Uptime/NewDetails/index.jsx deleted file mode 100644 index eca12ec57..000000000 --- a/Client/src/Pages/Uptime/NewDetails/index.jsx +++ /dev/null @@ -1,86 +0,0 @@ -// Components -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorHeader from "./Components/MonitorHeader"; -import StatusBoxes from "./Components/StatusBoxes"; -import TimeFramePicker from "./Components/TimeFramePicker"; -import ChartBoxes from "./Components/ChartBoxes"; -import ResponseTimeChart from "./Components/Charts/ResponseTimeChart"; -// MUI Components -import { Stack } from "@mui/material"; - -// Utils -import { useState } from "react"; -import { useParams } from "react-router-dom"; -import { useSelector } from "react-redux"; -import { useTheme } from "@emotion/react"; -import useMonitorFetch from "./Hooks/useMonitorFetch"; -import useCertificateFetch from "./Hooks/useCertificateFetch"; -// Constants -const BREADCRUMBS = [ - { name: "uptime", path: "/uptime" }, - { name: "details", path: "" }, - // { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy -]; - -const certificateDateFormat = "MMM D, YYYY h A"; - -const UptimeDetails = () => { - // Redux state - const { authToken } = useSelector((state) => state.auth); - const uiTimezone = useSelector((state) => state.ui.timezone); - - // Local state - const [dateRange, setDateRange] = useState("day"); - const [hoveredUptimeData, setHoveredUptimeData] = useState(null); - const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); - - // Utils - const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; - const { monitorId } = useParams(); - const theme = useTheme(); - - const { monitor, monitorIsLoading } = useMonitorFetch({ - authToken, - monitorId, - dateRange, - }); - - const { certificateExpiry, certificateIsLoading } = useCertificateFetch({ - monitor, - authToken, - monitorId, - certificateDateFormat, - uiTimezone, - }); - - return ( - - - - - - - - - ); -}; - -export default UptimeDetails; diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx index 6ac5624a9..575c108cf 100644 --- a/Client/src/Routes/index.jsx +++ b/Client/src/Routes/index.jsx @@ -6,7 +6,9 @@ import NotFound from "../Pages/NotFound"; import Login from "../Pages/Auth/Login/Login"; import Register from "../Pages/Auth/Register/Register"; import Account from "../Pages/Account"; -import Monitors from "../Pages/Uptime/Home"; +import Uptime from "../Pages/Uptime/Home"; +import UptimeDetails from "../Pages/Uptime/Details"; + import CreateMonitor from "../Pages/Uptime/CreateUptime"; import CreateInfrastructureMonitor from "../Pages/Infrastructure/CreateMonitor"; import Incidents from "../Pages/Incidents"; @@ -18,9 +20,6 @@ import CheckEmail from "../Pages/Auth/CheckEmail"; import SetNewPassword from "../Pages/Auth/SetNewPassword"; import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; import ProtectedRoute from "../Components/ProtectedRoute"; -import Details from "../Pages/Uptime/Details"; - -import UptimeDetails from "../Pages/Uptime/NewDetails"; import Maintenance from "../Pages/Maintenance"; import Configure from "../Pages/Uptime/Configure"; import PageSpeed from "../Pages/PageSpeed"; @@ -48,7 +47,7 @@ const Routes = () => { /> } + element={} /> Date: Mon, 27 Jan 2025 14:58:37 -0800 Subject: [PATCH 67/76] Add missing prop validation, cleanup --- .../Uptime/Details/Components/Charts/ChartBox.jsx | 1 + .../Components/Charts/ResponseTimeChart.jsx | 8 ++++++++ .../Details/Components/ConfigButton/index.jsx | 7 ++++++- .../Details/Components/ResponseTable/index.jsx | 1 + .../Details/Components/StatusBoxes/index.jsx | 15 +++++++++++---- .../Details/Components/TimeFramePicker/index.jsx | 7 +++++++ 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx index 672134781..3d4d73421 100644 --- a/Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx +++ b/Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx @@ -71,4 +71,5 @@ ChartBox.propTypes = { children: PropTypes.node, icon: PropTypes.node.isRequired, header: PropTypes.string.isRequired, + height: PropTypes.string, }; diff --git a/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index 73ffc035e..79e9473ef 100644 --- a/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -2,6 +2,8 @@ import ChartBox from "./ChartBox"; import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart"; import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react"; import SkeletonLayout from "./ResponseTimeChartSkeleton"; +import PropTypes from "prop-types"; + const ResponseTImeChart = ({ shouldRender = true, monitor, dateRange }) => { if (!shouldRender) { return ; @@ -20,4 +22,10 @@ const ResponseTImeChart = ({ shouldRender = true, monitor, dateRange }) => { ); }; +ResponseTImeChart.propTypes = { + shouldRender: PropTypes.bool, + monitor: PropTypes.object, + dateRange: PropTypes.string, +}; + export default ResponseTImeChart; diff --git a/Client/src/Pages/Uptime/Details/Components/ConfigButton/index.jsx b/Client/src/Pages/Uptime/Details/Components/ConfigButton/index.jsx index 9981b4e1c..467655abf 100644 --- a/Client/src/Pages/Uptime/Details/Components/ConfigButton/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/ConfigButton/index.jsx @@ -2,7 +2,7 @@ import { Button, Box } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react"; - +import PropTypes from "prop-types"; const ConfigButton = ({ shouldRender, monitorId }) => { const theme = useTheme(); const navigate = useNavigate(); @@ -32,4 +32,9 @@ const ConfigButton = ({ shouldRender, monitorId }) => { ); }; +ConfigButton.propTypes = { + shouldRender: PropTypes.bool, + monitorId: PropTypes.string, +}; + export default ConfigButton; diff --git a/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx index a510b9bf1..56ea8eca7 100644 --- a/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx @@ -76,6 +76,7 @@ const ResponseTable = ({ }; ResponseTable.propTypes = { + shouldRender: PropTypes.bool, checks: PropTypes.array.isRequired, checksCount: PropTypes.number.isRequired, uiTimezone: PropTypes.string.isRequired, diff --git a/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx index 55bcf34da..21922537f 100644 --- a/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx @@ -6,12 +6,15 @@ import SkeletonLayout from "./skeleton"; import { useTheme } from "@mui/material/styles"; import useUtils from "../../../Home/Hooks/useUtils"; import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; - +import PropTypes from "prop-types"; const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => { + // Utils + const theme = useTheme(); + const { determineState } = useUtils(); + if (!shouldRender) { return ; } - const theme = useTheme(); const { time: streakTime, units: streakUnits } = getHumanReadableDuration( monitor?.uptimeStreak ); @@ -20,8 +23,6 @@ const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => { monitor?.timeSinceLastCheck ); - const { determineState } = useUtils(); - return ( { ); }; +StatusBoxes.propTypes = { + shouldRender: PropTypes.bool, + monitor: PropTypes.object, + certificateExpiry: PropTypes.string, +}; + export default StatusBoxes; diff --git a/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx b/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx index ade677604..789030282 100644 --- a/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx @@ -1,6 +1,7 @@ import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; import { useTheme } from "@emotion/react"; import SkeletonLayout from "./skeleton"; +import PropTypes from "prop-types"; const TimeFramePicker = ({ shouldRender = true, dateRange, setDateRange }) => { const theme = useTheme(); @@ -48,4 +49,10 @@ const TimeFramePicker = ({ shouldRender = true, dateRange, setDateRange }) => { ); }; +TimeFramePicker.propTypes = { + shouldRender: PropTypes.bool, + dateRange: PropTypes.string, + setDateRange: PropTypes.func, +}; + export default TimeFramePicker; From fe4f0fae355b69498ed09cd093c144ed322529ba Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 28 Jan 2025 01:32:04 +0000 Subject: [PATCH 68/76] Update dependency recharts to v2.15.1 --- Client/package-lock.json | 20 ++++++++++---------- Client/package.json | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index 7d2a2427c..597b38a9c 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -28,7 +28,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.15.0", + "recharts": "2.15.1", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, @@ -5479,9 +5479,9 @@ } }, "node_modules/react-smooth": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", - "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", @@ -5489,8 +5489,8 @@ "react-transition-group": "^4.4.5" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-toastify": { @@ -5523,16 +5523,16 @@ } }, "node_modules/recharts": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", - "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", - "react-smooth": "^4.0.0", + "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" diff --git a/Client/package.json b/Client/package.json index 1e2647a03..3192e1f3b 100644 --- a/Client/package.json +++ b/Client/package.json @@ -31,7 +31,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.15.0", + "recharts": "2.15.1", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, From 128745019dce0b3179854153f256fa8d6fe5d0e8 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 28 Jan 2025 12:59:08 +0000 Subject: [PATCH 69/76] Update dependency bullmq to v5.38.0 --- Server/package-lock.json | 8 ++++---- Server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Server/package-lock.json b/Server/package-lock.json index d5a5ea747..fdbd6c19c 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.37.0", + "bullmq": "5.38.0", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", @@ -1748,9 +1748,9 @@ } }, "node_modules/bullmq": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.37.0.tgz", - "integrity": "sha512-h/wf979+9uROyYpB8oTE44Py6JERCluCSd+ZFpCZlPsYh+wxAkqrfHsHHHKBgsNJp9odWLIY4SG+280EXzXiCQ==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.38.0.tgz", + "integrity": "sha512-X6ApLQlSmX0LQ58xCpCoXE2nN57y8H9LD4EApJl9+8E6+FNWdKy6gykL8ofB9x4TcZeeGJhZOUZJAuhRV721bw==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", diff --git a/Server/package.json b/Server/package.json index 9d92a2010..0f63d97fe 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.37.0", + "bullmq": "5.38.0", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", From 66de46f695e460016db7d720358a586ca233a49a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jan 2025 10:58:03 -0800 Subject: [PATCH 70/76] Remove basic table component --- Client/src/Components/BasicTable/index.jsx | 360 --------------------- 1 file changed, 360 deletions(-) delete mode 100644 Client/src/Components/BasicTable/index.jsx diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx deleted file mode 100644 index b93c584da..000000000 --- a/Client/src/Components/BasicTable/index.jsx +++ /dev/null @@ -1,360 +0,0 @@ -/* TODO - -Should we delete this? -*/ - -import PropTypes from "prop-types"; -import { useState, useEffect } from "react"; -import { useTheme } from "@emotion/react"; -import { - TableContainer, - Paper, - Table, - TableHead, - TableRow, - TableCell, - TableBody, - TablePagination, - Box, - Typography, - Stack, - Button, -} from "@mui/material"; -import { useDispatch, useSelector } from "react-redux"; -import { setRowsPerPage } from "../../Features/UI/uiSlice"; -import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react"; -import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react"; -import LeftArrow from "../../assets/icons/left-arrow.svg?react"; -import RightArrow from "../../assets/icons/right-arrow.svg?react"; -import SelectorVertical from "../../assets/icons/selector-vertical.svg?react"; -import "./index.css"; -/** - * Component for pagination actions (first, previous, next, last). - * - * @component - * @param {Object} props - * @param {number} props.count - Total number of items. - * @param {number} props.page - Current page number. - * @param {number} props.rowsPerPage - Number of rows per page. - * @param {function} props.onPageChange - Callback function to handle page change. - * - * @returns {JSX.Element} Pagination actions component. - */ -const TablePaginationActions = (props) => { - const { count, page, rowsPerPage, onPageChange } = props; - - const handleFirstPageButtonClick = (event) => { - onPageChange(event, 0); - }; - const handleBackButtonClick = (event) => { - onPageChange(event, page - 1); - }; - const handleNextButtonClick = (event) => { - onPageChange(event, page + 1); - }; - const handleLastPageButtonClick = (event) => { - onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); - }; - - return ( - - - - - - - ); -}; - -TablePaginationActions.propTypes = { - count: PropTypes.number.isRequired, - page: PropTypes.number.isRequired, - rowsPerPage: PropTypes.number.isRequired, - onPageChange: PropTypes.func.isRequired, -}; - -/** - * BasicTable Component - * Renders a table with optional pagination. - * - * @component - * @param {Object} props - Component props. - * @param {Object} props.data - Data for the table including columns and rows. - * @param {Array} props.data.cols - Array of objects for column headers. - * @param {number} props.data.cols[].id - Unique identifier for the column. - * @param {string} props.data.cols[].name - Name of the column to display as header. - * @param {Array} props.data.rows - Array of row objects. - * @param {number} props.data.rows[].id - Unique identifier for the row. - * @param {Array} props.data.rows[].data - Array of cell data objects for the row. - * @param {number} props.data.rows[].data[].id - Unique identifier for the cell. - * @param {JSX.Element} props.data.rows[].data[].data - The content to display in the cell. - * @param {function} props.data.rows.data.handleClick - Function to call when the row is clicked. - * @param {boolean} [props.paginated=false] - Flag to enable pagination. - * @param {boolean} [props.reversed=false] - Flag to enable reverse order. - * @param {number} props.rowsPerPage- Number of rows per page (table). - * @param {string} props.emptyMessage - Message to display when there is no data. - * @example - * const data = { - * cols: [ - * { id: 1, name: "First Col" }, - * { id: 2, name: "Second Col" }, - * { id: 3, name: "Third Col" }, - * { id: 4, name: "Fourth Col" }, - * ], - * rows: [ - * { - * id: 1, - * data: [ - * { id: 1, data:
Data for Row 1 Col 1
}, - * { id: 2, data:
Data for Row 1 Col 2
}, - * { id: 3, data:
Data for Row 1 Col 3
}, - * { id: 4, data:
Data for Row 1 Col 4
}, - * ], - * }, - * { - * id: 2, - * data: [ - * { id: 5, data:
Data for Row 2 Col 1
}, - * { id: 6, data:
Data for Row 2 Col 2
}, - * { id: 7, data:
Data for Row 2 Col 3
}, - * { id: 8, data:
Data for Row 2 Col 4
}, - * ], - * }, - * ], - * }; - * - * - */ - -const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => { - const DEFAULT_ROWS_PER_PAGE = 5; - const theme = useTheme(); - const dispatch = useDispatch(); - const uiState = useSelector((state) => state.ui); - let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE; - const [page, setPage] = useState(0); - - useEffect(() => { - setPage(0); - }, [data]); - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: table, - }) - ); - setPage(0); - }; - - let displayData = []; - - if (data && data.rows) { - let rows = reversed ? [...data.rows].reverse() : data.rows; - displayData = paginated - ? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - : rows; - } - - if (!data || !data.cols || !data.rows) { - return
No data
; - } - - /** - * Helper function to calculate the range of displayed rows. - * @returns {string} - */ - const getRange = () => { - let start = page * rowsPerPage + 1; - let end = Math.min(page * rowsPerPage + rowsPerPage, data.rows.length); - return `${start} - ${end}`; - }; - - return ( - <> - - - - - {data.cols.map((col) => ( - {col.name} - ))} - - - - {displayData.map((row) => { - return ( - - {row.data.map((cell) => { - return {cell.data}; - })} - - ); - })} - {displayData.length === 0 && ( - - - {emptyMessage} - - - )} - -
-
- {paginated && ( - - - Showing {getRange()} of {data.rows.length} monitor(s) - - - `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}` - } - slotProps={{ - select: { - MenuProps: { - keepMounted: true, - PaperProps: { - className: "pagination-dropdown", - sx: { - mt: 0, - mb: theme.spacing(2), - }, - }, - transformOrigin: { vertical: "bottom", horizontal: "left" }, - anchorOrigin: { vertical: "top", horizontal: "left" }, - sx: { mt: theme.spacing(-2) }, - }, - inputProps: { id: "pagination-dropdown" }, - IconComponent: SelectorVertical, - sx: { - ml: theme.spacing(4), - mr: theme.spacing(12), - minWidth: theme.spacing(20), - textAlign: "left", - "&.Mui-focused > div": { - backgroundColor: theme.palette.primary.main, - }, - }, - }, - }} - sx={{ - mt: theme.spacing(6), - color: theme.palette.primary.contrastTextSecondary, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - strokeWidth: 1.3, - }, - "& .MuiSelect-select": { - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - }, - }} - /> - - )} - - ); -}; - -BasicTable.propTypes = { - data: PropTypes.object.isRequired, - paginated: PropTypes.bool, - reversed: PropTypes.bool, - rowPerPage: PropTypes.number, - table: PropTypes.string, - emptyMessage: PropTypes.string, -}; - -export default BasicTable; From e67b9ecb09231e7c454c2518f6ea823eb08b7204 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jan 2025 10:58:19 -0800 Subject: [PATCH 71/76] update fallback component --- Client/src/Components/Fallback/index.jsx | 105 +++++++++++++---------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/Client/src/Components/Fallback/index.jsx b/Client/src/Components/Fallback/index.jsx index 61840452f..48d9e2198 100644 --- a/Client/src/Components/Fallback/index.jsx +++ b/Client/src/Components/Fallback/index.jsx @@ -26,58 +26,69 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false }) => const mode = useSelector((state) => state.ui.mode); return ( - - {mode === "light" ? ( - - ) : ( - - )} - - - - + ) : ( + + )} + - {vowelStart ? "An" : "A"} {title} is used to: - - {checks.map((check, index) => ( - - ))} + +
+ + + {vowelStart ? "An" : "A"} {title} is used to: + + {checks.map((check, index) => ( + + ))} + + {/* TODO - display a different fallback if user is not an admin*/} + {isAdmin && ( + + )} - {/* TODO - display a different fallback if user is not an admin*/} - {isAdmin && ( - - )} - + ); }; From ac669d8fe14bd982424dbf26bd0308f4c8501706 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jan 2025 11:03:15 -0800 Subject: [PATCH 72/76] standardize directory structure --- .../Components}/CustomThreshold/index.jsx | 4 ++-- .../Pages/Infrastructure/{CreateMonitor => Create}/index.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename Client/src/Pages/Infrastructure/{CreateMonitor => Create/Components}/CustomThreshold/index.jsx (96%) rename Client/src/Pages/Infrastructure/{CreateMonitor => Create}/index.jsx (99%) diff --git a/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx b/Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx similarity index 96% rename from Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx rename to Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx index 3220582a8..922cb46d8 100644 --- a/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx +++ b/Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx @@ -1,6 +1,6 @@ import { Box, Stack, Typography } from "@mui/material"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import Checkbox from "../../../../Components/Inputs/Checkbox"; +import TextInput from "../../../../../Components/Inputs/TextInput"; +import Checkbox from "../../../../../Components/Inputs/Checkbox"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; diff --git a/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx b/Client/src/Pages/Infrastructure/Create/index.jsx similarity index 99% rename from Client/src/Pages/Infrastructure/CreateMonitor/index.jsx rename to Client/src/Pages/Infrastructure/Create/index.jsx index c51b56360..464bdeceb 100644 --- a/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx +++ b/Client/src/Pages/Infrastructure/Create/index.jsx @@ -22,7 +22,7 @@ import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; import { createToast } from "../../../Utils/toastUtils"; import Checkbox from "../../../Components/Inputs/Checkbox"; import Select from "../../../Components/Inputs/Select"; -import { CustomThreshold } from "./CustomThreshold"; +import { CustomThreshold } from "./Components/CustomThreshold"; const SELECT_VALUES = [ { _id: 0.25, name: "15 seconds" }, From 98409991831a3c356de84821062d44196d3058c9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jan 2025 11:03:46 -0800 Subject: [PATCH 73/76] standardize uptime directory structure, use shared components --- .../Uptime/{CreateUptime => Create}/index.css | 0 .../Uptime/{CreateUptime => Create}/index.jsx | 0 .../Home/Components/StatusBoxes/index.jsx | 2 +- .../Home/Components/UptimeDataTable/index.jsx | 16 +++++++------- Client/src/Pages/Uptime/Home/index.jsx | 21 +++++++++++++++++-- 5 files changed, 28 insertions(+), 11 deletions(-) rename Client/src/Pages/Uptime/{CreateUptime => Create}/index.css (100%) rename Client/src/Pages/Uptime/{CreateUptime => Create}/index.jsx (100%) diff --git a/Client/src/Pages/Uptime/CreateUptime/index.css b/Client/src/Pages/Uptime/Create/index.css similarity index 100% rename from Client/src/Pages/Uptime/CreateUptime/index.css rename to Client/src/Pages/Uptime/Create/index.css diff --git a/Client/src/Pages/Uptime/CreateUptime/index.jsx b/Client/src/Pages/Uptime/Create/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/CreateUptime/index.jsx rename to Client/src/Pages/Uptime/Create/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx index ac9eca47f..aa7b7ee0d 100644 --- a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx +++ b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx @@ -30,7 +30,7 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => { }; StatusBoxes.propTypes = { - monitorsSummary: PropTypes.object.isRequired, + monitorsSummary: PropTypes.object, }; export default StatusBoxes; diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx index 936a6ebed..e96705cc1 100644 --- a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx @@ -12,7 +12,9 @@ import { useState } from "react"; import SearchComponent from "../SearchComponent"; import CircularCount from "../../../../../Components/CircularCount"; import LoadingSpinner from "../LoadingSpinner"; +import MonitorCountHeader from "../../../../../Components/MonitorCountHeader"; import UptimeDataTableSkeleton from "./skeleton"; + // Utils import { useTheme } from "@emotion/react"; import useUtils from "../../Hooks/useUtils"; @@ -214,21 +216,19 @@ const UptimeDataTable = (props) => { flex={1} py={theme.spacing(8)} > - - Uptime monitors - - + { }); const totalMonitors = monitorsSummary?.totalMonitors ?? 0; + if (!isLoading && monitors?.length === 0) { + return ( + + ); + } + return ( - + Date: Tue, 28 Jan 2025 11:04:11 -0800 Subject: [PATCH 74/76] Refactor pagespeed page --- .../Components/CreateMonitorHeader/index.jsx | 33 ++++ .../CreateMonitorHeader/skeleton.jsx | 19 +++ .../Components/MonitorCountHeader/index.jsx | 40 +++++ .../MonitorCountHeader/skeleton.jsx | 26 +++ .../{CreatePageSpeed => Create}/index.css | 0 .../{CreatePageSpeed => Create}/index.jsx | 0 .../Components/Card/index.jsx} | 11 +- .../Monitors/Components/MonitorGrid/index.jsx | 22 +++ .../Monitors/Hooks/useMonitorsFetch.jsx | 43 +++++ Client/src/Pages/PageSpeed/Monitors/index.jsx | 66 ++++++++ Client/src/Pages/PageSpeed/index.css | 8 - Client/src/Pages/PageSpeed/index.jsx | 153 ------------------ Client/src/Pages/PageSpeed/skeleton.jsx | 73 --------- Client/src/Routes/index.jsx | 67 ++++---- 14 files changed, 295 insertions(+), 266 deletions(-) create mode 100644 Client/src/Components/CreateMonitorHeader/index.jsx create mode 100644 Client/src/Components/CreateMonitorHeader/skeleton.jsx create mode 100644 Client/src/Components/MonitorCountHeader/index.jsx create mode 100644 Client/src/Components/MonitorCountHeader/skeleton.jsx rename Client/src/Pages/PageSpeed/{CreatePageSpeed => Create}/index.css (100%) rename Client/src/Pages/PageSpeed/{CreatePageSpeed => Create}/index.jsx (100%) rename Client/src/Pages/PageSpeed/{card.jsx => Monitors/Components/Card/index.jsx} (94%) create mode 100644 Client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx create mode 100644 Client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx create mode 100644 Client/src/Pages/PageSpeed/Monitors/index.jsx delete mode 100644 Client/src/Pages/PageSpeed/index.css delete mode 100644 Client/src/Pages/PageSpeed/index.jsx delete mode 100644 Client/src/Pages/PageSpeed/skeleton.jsx diff --git a/Client/src/Components/CreateMonitorHeader/index.jsx b/Client/src/Components/CreateMonitorHeader/index.jsx new file mode 100644 index 000000000..b8fcbc39a --- /dev/null +++ b/Client/src/Components/CreateMonitorHeader/index.jsx @@ -0,0 +1,33 @@ +import { Stack, Button } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import PropTypes from "prop-types"; +import SkeletonLayout from "./skeleton"; + +const CreateMonitorHeader = ({ isAdmin, shouldRender, path }) => { + const navigate = useNavigate(); + if (!isAdmin) return null; + if (!shouldRender) return ; + return ( + + + + ); +}; + +export default CreateMonitorHeader; + +CreateMonitorHeader.propTypes = { + isAdmin: PropTypes.bool.isRequired, + shouldRender: PropTypes.bool.isRequired, + path: PropTypes.string.isRequired, +}; diff --git a/Client/src/Components/CreateMonitorHeader/skeleton.jsx b/Client/src/Components/CreateMonitorHeader/skeleton.jsx new file mode 100644 index 000000000..7583f7849 --- /dev/null +++ b/Client/src/Components/CreateMonitorHeader/skeleton.jsx @@ -0,0 +1,19 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Components/MonitorCountHeader/index.jsx b/Client/src/Components/MonitorCountHeader/index.jsx new file mode 100644 index 000000000..f7c31a5a0 --- /dev/null +++ b/Client/src/Components/MonitorCountHeader/index.jsx @@ -0,0 +1,40 @@ +import { Stack } from "@mui/material"; +import { Heading } from "../Heading"; +import CircularCount from "../CircularCount"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton"; + +const MonitorCountHeader = ({ + shouldRender = true, + monitorCount, + heading, + sx, + children, +}) => { + const theme = useTheme(); + if (!shouldRender) return ; + + return ( + + {heading} + + {children} + + ); +}; + +MonitorCountHeader.propTypes = { + shouldRender: PropTypes.bool, + monitorCount: PropTypes.number, + heading: PropTypes.string, + children: PropTypes.node, + sx: PropTypes.object, +}; + +export default MonitorCountHeader; diff --git a/Client/src/Components/MonitorCountHeader/skeleton.jsx b/Client/src/Components/MonitorCountHeader/skeleton.jsx new file mode 100644 index 000000000..9b4971c32 --- /dev/null +++ b/Client/src/Components/MonitorCountHeader/skeleton.jsx @@ -0,0 +1,26 @@ +import { Stack, Skeleton } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css b/Client/src/Pages/PageSpeed/Create/index.css similarity index 100% rename from Client/src/Pages/PageSpeed/CreatePageSpeed/index.css rename to Client/src/Pages/PageSpeed/Create/index.css diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/Create/index.jsx similarity index 100% rename from Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx rename to Client/src/Pages/PageSpeed/Create/index.jsx diff --git a/Client/src/Pages/PageSpeed/card.jsx b/Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx similarity index 94% rename from Client/src/Pages/PageSpeed/card.jsx rename to Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx index f7f6c4249..fe05da514 100644 --- a/Client/src/Pages/PageSpeed/card.jsx +++ b/Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx @@ -1,15 +1,16 @@ import PropTypes from "prop-types"; -import PageSpeedIcon from "../../assets/icons/page-speed.svg?react"; -import { StatusLabel } from "../../Components/Label"; +// import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react"; +import PageSpeedIcon from "../../../../../assets/icons/page-speed.svg?react"; +import { StatusLabel } from "../../../../../Components/Label"; import { Box, Grid, Stack, Typography } from "@mui/material"; import { useNavigate } from "react-router"; import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; -import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils"; -import useUtils from "../Uptime/Home/Hooks/useUtils"; +import { formatDateWithTz, formatDurationSplit } from "../../../../../Utils/timeUtils"; +import useUtils from "../../../../Uptime/Home/Hooks/useUtils"; import { useState } from "react"; -import IconBox from "../../Components/IconBox"; +import IconBox from "../../../../../Components/IconBox"; /** * CustomToolTip displays a tooltip with formatted date and score information. * @param {Object} props diff --git a/Client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/Client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx new file mode 100644 index 000000000..0e61c9c15 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx @@ -0,0 +1,22 @@ +import { Grid, Grid2 } from "@mui/material"; +import Card from "../Card"; + +const MonitorGrid = ({ shouldRender, monitors }) => { + if (!shouldRender) return null; + + return ( + + {monitors?.map((monitor) => ( + + ))} + + ); +}; + +export default MonitorGrid; diff --git a/Client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx b/Client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx new file mode 100644 index 000000000..06d423acc --- /dev/null +++ b/Client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; + +const useMonitorsFetch = ({ authToken, teamId }) => { + const [isLoading, setIsLoading] = useState(true); + const [monitors, setMonitors] = useState([]); + const [summary, setSummary] = useState({}); + + useEffect(() => { + const fetchMonitors = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorsByTeamId({ + authToken: authToken, + teamId: teamId, + limit: 10, + types: ["pagespeed"], + page: null, + rowsPerPage: null, + filter: null, + field: null, + order: null, + }); + if (res?.data?.data?.filteredMonitors) { + setMonitors(res.data.data.filteredMonitors); + setSummary(res.data.data.summary); + } + } catch (error) { + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + + fetchMonitors(); + }, [authToken, teamId]); + return { isLoading, monitors, summary }; +}; + +export default useMonitorsFetch; diff --git a/Client/src/Pages/PageSpeed/Monitors/index.jsx b/Client/src/Pages/PageSpeed/Monitors/index.jsx new file mode 100644 index 000000000..9376d07ce --- /dev/null +++ b/Client/src/Pages/PageSpeed/Monitors/index.jsx @@ -0,0 +1,66 @@ +// Components +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import { Stack, Box } from "@mui/material"; +import CreateMonitorHeader from "../../../Components/CreateMonitorHeader"; +import MonitorCountHeader from "../../../Components/MonitorCountHeader"; +import MonitorGrid from "./Components/MonitorGrid"; +import Fallback from "../../../Components/Fallback"; + +// Utils +import { useTheme } from "@emotion/react"; +import { useSelector } from "react-redux"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import useMonitorsFetch from "./Hooks/useMonitorsFetch"; + +// Constants +const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; + +const NewPageSpeed = () => { + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const { user, authToken } = useSelector((state) => state.auth); + + const { isLoading, monitors, summary } = useMonitorsFetch({ + authToken: authToken, + teamId: user.teamId, + }); + + if (!isLoading && monitors?.length === 0) { + return ( + + ); + } + + return ( + + + + + + + ); +}; + +export default NewPageSpeed; diff --git a/Client/src/Pages/PageSpeed/index.css b/Client/src/Pages/PageSpeed/index.css deleted file mode 100644 index 3741a6f0f..000000000 --- a/Client/src/Pages/PageSpeed/index.css +++ /dev/null @@ -1,8 +0,0 @@ -.page-speed .label { - padding: 7px; - height: 24px; - font-size: var(--env-var-font-size-small); -} -.page-speed:not(:has([class*="fallback__"])) button { - min-height: 34px; -} diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx deleted file mode 100644 index c3d37e2c0..000000000 --- a/Client/src/Pages/PageSpeed/index.jsx +++ /dev/null @@ -1,153 +0,0 @@ -import { Box, Button, Grid, Stack } from "@mui/material"; -import { useEffect, useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useSelector } from "react-redux"; -import Fallback from "../../Components/Fallback"; -import "./index.css"; -import { useNavigate } from "react-router"; -import PropTypes from "prop-types"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import SkeletonLayout from "./skeleton"; -import Card from "./card"; -import { networkService } from "../../main"; -import { Heading } from "../../Components/Heading"; -import { useIsAdmin } from "../../Hooks/useIsAdmin"; -const PageSpeed = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const isAdmin = useIsAdmin(); - const { user, authToken } = useSelector((state) => state.auth); - const [isLoading, setIsLoading] = useState(true); - const [monitors, setMonitors] = useState([]); - const [summary, setSummary] = useState({}); - - useEffect(() => { - const fetchMonitors = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorsByTeamId({ - authToken: authToken, - teamId: user.teamId, - limit: 10, - types: ["pagespeed"], - page: null, - rowsPerPage: null, - filter: null, - field: null, - order: null, - }); - if (res?.data?.data?.filteredMonitors) { - setMonitors(res.data.data.filteredMonitors); - setSummary(res.data.data.summary); - } - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } - }; - - fetchMonitors(); - }, [authToken, user.teamId]); - - // will show skeletons only on initial load - // since monitor state is being added to redux persist, there's no reason to display skeletons on every render - let isActuallyLoading = isLoading && monitors?.length === 0; - return ( - [class*="fallback__"])': { - position: "relative", - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - borderStyle: "dashed", - backgroundColor: theme.palette.primary.main, - overflow: "hidden", - }, - }} - > - {isActuallyLoading ? ( - - ) : monitors?.length !== 0 ? ( - - - - - {isAdmin && ( - - )} - - - - PageSpeed monitors - {/* TODO Correct the class current-monitors-counter, there are some unnecessary things there */} - - {summary?.totalMonitors ?? 0} - - - - {monitors?.map((monitor) => ( - - ))} - - - ) : ( - - )} - - ); -}; -PageSpeed.propTypes = { - isAdmin: PropTypes.bool, -}; - -export default PageSpeed; diff --git a/Client/src/Pages/PageSpeed/skeleton.jsx b/Client/src/Pages/PageSpeed/skeleton.jsx deleted file mode 100644 index b6d29dc38..000000000 --- a/Client/src/Pages/PageSpeed/skeleton.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Box, Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx index 575c108cf..df0af2618 100644 --- a/Client/src/Routes/index.jsx +++ b/Client/src/Routes/index.jsx @@ -1,36 +1,49 @@ import { Navigate, Route, Routes as LibRoutes } from "react-router"; import HomeLayout from "../Components/Layouts/HomeLayout"; import { Infrastructure } from "../Pages/Infrastructure"; -import InfrastructureDetails from "../Pages/Infrastructure/Details"; import NotFound from "../Pages/NotFound"; -import Login from "../Pages/Auth/Login/Login"; -import Register from "../Pages/Auth/Register/Register"; -import Account from "../Pages/Account"; + +// Auth +import AuthLogin from "../Pages/Auth/Login/Login"; +import AuthRegister from "../Pages/Auth/Register/Register"; +import AuthForgotPassword from "../Pages/Auth/ForgotPassword"; +import AuthCheckEmail from "../Pages/Auth/CheckEmail"; +import AuthSetNewPassword from "../Pages/Auth/SetNewPassword"; +import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; + +// Uptime import Uptime from "../Pages/Uptime/Home"; import UptimeDetails from "../Pages/Uptime/Details"; +import UptimeCreate from "../Pages/Uptime/Create"; +import UptimeConfigure from "../Pages/Uptime/Configure"; + +// PageSpeed +// import PageSpeed from "../Pages/PageSpeed"; +import PageSpeed from "../Pages/PageSpeed/Monitors"; +import PageSpeedCreate from "../Pages/PageSpeed/Create"; +import PageSpeedDetails from "../Pages/PageSpeed/Details"; +import PageSpeedConfigure from "../Pages/PageSpeed/Configure"; + +// Infrastructure +import InfrastructureCreate from "../Pages/Infrastructure/Create"; +import InfrastructureDetails from "../Pages/Infrastructure/Details"; -import CreateMonitor from "../Pages/Uptime/CreateUptime"; -import CreateInfrastructureMonitor from "../Pages/Infrastructure/CreateMonitor"; import Incidents from "../Pages/Incidents"; import Status from "../Pages/Status"; import Integrations from "../Pages/Integrations"; + +// Settings +import Account from "../Pages/Account"; import Settings from "../Pages/Settings"; -import ForgotPassword from "../Pages/Auth/ForgotPassword"; -import CheckEmail from "../Pages/Auth/CheckEmail"; -import SetNewPassword from "../Pages/Auth/SetNewPassword"; -import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; -import ProtectedRoute from "../Components/ProtectedRoute"; + import Maintenance from "../Pages/Maintenance"; -import Configure from "../Pages/Uptime/Configure"; -import PageSpeed from "../Pages/PageSpeed"; -import CreatePageSpeed from "../Pages/PageSpeed/CreatePageSpeed"; + +import ProtectedRoute from "../Components/ProtectedRoute"; import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance"; -import PageSpeedDetails from "../Pages/PageSpeed/Details"; -import PageSpeedConfigure from "../Pages/PageSpeed/Configure"; import withAdminCheck from "../Components/HOC/withAdminCheck"; const Routes = () => { - const AdminCheckedRegister = withAdminCheck(Register); + const AdminCheckedRegister = withAdminCheck(AuthRegister); return ( { } + element={} /> { /> } + element={} /> { /> } + element={} /> { /> } + element={} /> { } + element={} /> { } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> Date: Tue, 28 Jan 2025 11:05:06 -0800 Subject: [PATCH 75/76] standardize directory names --- Client/src/Components/StatBox/index.jsx | 2 +- .../src/Pages/Infrastructure/Details/index.jsx | 2 +- Client/src/Pages/Infrastructure/index.jsx | 17 +++++++++-------- Client/src/Pages/PageSpeed/Configure/index.jsx | 2 +- Client/src/Pages/PageSpeed/Details/index.jsx | 2 +- .../Monitors/Components/Card/index.jsx | 2 +- .../Details/Components/MonitorHeader/index.jsx | 2 +- .../Details/Components/StatusBoxes/index.jsx | 2 +- .../Components/ActionsMenu/index.jsx | 0 .../Components/Host/index.jsx | 0 .../Components/LoadingSpinner/index.jsx | 0 .../Components/SearchComponent/index.jsx | 0 .../Components/Skeleton/index.jsx | 0 .../Components/StatusBoxes/index.jsx | 0 .../Components/StatusBoxes/skeleton.jsx | 0 .../Components/StatusBoxes/statusBox.jsx | 0 .../Components/UptimeDataTable/index.jsx | 0 .../Components/UptimeDataTable/skeleton.jsx | 0 .../{Home => Monitors}/Hooks/useDebounce.jsx | 0 .../Hooks/useMonitorsFetch.jsx | 0 .../{Home => Monitors}/Hooks/useUtils.jsx | 0 .../Pages/Uptime/{Home => Monitors}/index.jsx | 0 Client/src/Routes/index.jsx | 2 +- 23 files changed, 17 insertions(+), 16 deletions(-) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/ActionsMenu/index.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/Host/index.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/LoadingSpinner/index.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/SearchComponent/index.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/Skeleton/index.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/StatusBoxes/index.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/StatusBoxes/skeleton.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/StatusBoxes/statusBox.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/UptimeDataTable/index.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Components/UptimeDataTable/skeleton.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Hooks/useDebounce.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Hooks/useMonitorsFetch.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/Hooks/useUtils.jsx (100%) rename Client/src/Pages/Uptime/{Home => Monitors}/index.jsx (100%) diff --git a/Client/src/Components/StatBox/index.jsx b/Client/src/Components/StatBox/index.jsx index b476a1c33..4b1fd2454 100644 --- a/Client/src/Components/StatBox/index.jsx +++ b/Client/src/Components/StatBox/index.jsx @@ -1,7 +1,7 @@ import { Box, Typography } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import useUtils from "../../Pages/Uptime/Home/Hooks/useUtils"; +import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; /** * StatBox Component diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index 07fbd7c06..9d6b09330 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -8,7 +8,7 @@ import AreaChart from "../../../Components/Charts/AreaChart"; import { useSelector } from "react-redux"; import { networkService } from "../../../main"; import PulseDot from "../../../Components/Animated/PulseDot"; -import useUtils from "../../Uptime/Home/Hooks/useUtils"; +import useUtils from "../../Uptime/Monitors/Hooks/useUtils"; import { useNavigate } from "react-router-dom"; import Empty from "./empty"; import { logger } from "../../../Utils/Logger"; diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 3f295d434..91367ce22 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { /* useDispatch, */ useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; -import useUtils from "../Uptime/Home/Hooks/useUtils.jsx"; +import useUtils from "../Uptime/Monitors/Hooks/useUtils.jsx"; import { jwtDecode } from "jwt-decode"; import SkeletonLayout from "./skeleton"; import Fallback from "../../Components/Fallback"; @@ -17,7 +17,7 @@ import Pagination from "../../Components/Table/TablePagination/index.jsx"; // import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice"; import { networkService } from "../../Utils/NetworkService.js"; import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx"; -import Host from "../Uptime/Home/Components/Host"; +import Host from "../Uptime/Monitors/Components/Host/index.jsx"; import { useIsAdmin } from "../../Hooks/useIsAdmin.js"; import { InfrastructureMenu } from "./components/Menu"; @@ -156,12 +156,13 @@ function Infrastructure() { const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100) .toFixed(2) .toString(); - const percentageColor = monitor.uptimePercentage < 0.25 - ? theme.palette.error.main - : monitor.uptimePercentage < 0.5 - ? theme.palette.warning.main - : theme.palette.success.main; - + const percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.error.main + : monitor.uptimePercentage < 0.5 + ? theme.palette.warning.main + : theme.palette.success.main; + return { id: monitor._id, name: monitor.name, diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx index eee6dfcc9..a76f496cb 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.jsx +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import LoadingButton from "@mui/lab/LoadingButton"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; import SkeletonLayout from "./skeleton"; -import useUtils from "../../Uptime/Home/Hooks/useUtils"; +import useUtils from "../../Uptime/Monitors/Hooks/useUtils"; import "./index.css"; import Dialog from "../../../Components/Dialog"; diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx index 9192faaa9..4c4bceb1b 100644 --- a/Client/src/Pages/PageSpeed/Details/index.jsx +++ b/Client/src/Pages/PageSpeed/Details/index.jsx @@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import PagespeedDetailsAreaChart from "./Charts/AreaChart"; import Checkbox from "../../../Components/Inputs/Checkbox"; import PieChart from "./Charts/PieChart"; -import useUtils from "../../Uptime/Home/Hooks/useUtils"; +import useUtils from "../../Uptime/Monitors/Hooks/useUtils"; import "./index.css"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import StatBox from "../../../Components/StatBox"; diff --git a/Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx index fe05da514..42abca268 100644 --- a/Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx +++ b/Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx @@ -8,7 +8,7 @@ import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; import { formatDateWithTz, formatDurationSplit } from "../../../../../Utils/timeUtils"; -import useUtils from "../../../../Uptime/Home/Hooks/useUtils"; +import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils"; import { useState } from "react"; import IconBox from "../../../../../Components/IconBox"; /** diff --git a/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx index fddf9c3e5..53d4c4dfa 100644 --- a/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx @@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material"; import PulseDot from "../../../../../Components/Animated/PulseDot"; import Dot from "../../../../../Components/Dot"; import { useTheme } from "@emotion/react"; -import useUtils from "../../../Home/Hooks/useUtils"; +import useUtils from "../../../Monitors/Hooks/useUtils"; import { formatDurationRounded } from "../../../../../Utils/timeUtils"; import ConfigButton from "../ConfigButton"; import SkeletonLayout from "./skeleton"; diff --git a/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx index 21922537f..3f483889b 100644 --- a/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx +++ b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx @@ -4,7 +4,7 @@ import StatBox from "../../../../../Components/StatBox"; import SkeletonLayout from "./skeleton"; // Utils import { useTheme } from "@mui/material/styles"; -import useUtils from "../../../Home/Hooks/useUtils"; +import useUtils from "../../../Monitors/Hooks/useUtils"; import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; import PropTypes from "prop-types"; const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => { diff --git a/Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/ActionsMenu/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx rename to Client/src/Pages/Uptime/Monitors/Components/ActionsMenu/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/Host/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/Host/index.jsx rename to Client/src/Pages/Uptime/Monitors/Components/Host/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx rename to Client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx rename to Client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx rename to Client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx rename to Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx rename to Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx rename to Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx rename to Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx b/Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/skeleton.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx rename to Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/skeleton.jsx diff --git a/Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx b/Client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx rename to Client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx diff --git a/Client/src/Pages/Uptime/Home/Hooks/useMonitorsFetch.jsx b/Client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Hooks/useMonitorsFetch.jsx rename to Client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx diff --git a/Client/src/Pages/Uptime/Home/Hooks/useUtils.jsx b/Client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/Hooks/useUtils.jsx rename to Client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Monitors/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/index.jsx rename to Client/src/Pages/Uptime/Monitors/index.jsx diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx index df0af2618..a4df5b855 100644 --- a/Client/src/Routes/index.jsx +++ b/Client/src/Routes/index.jsx @@ -12,7 +12,7 @@ import AuthSetNewPassword from "../Pages/Auth/SetNewPassword"; import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; // Uptime -import Uptime from "../Pages/Uptime/Home"; +import Uptime from "../Pages/Uptime/Monitors"; import UptimeDetails from "../Pages/Uptime/Details"; import UptimeCreate from "../Pages/Uptime/Create"; import UptimeConfigure from "../Pages/Uptime/Configure"; From 5880993565b04a9a91e63b3bb878ed0e34540377 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jan 2025 11:06:24 -0800 Subject: [PATCH 76/76] fix PageSpeed name --- Client/src/Pages/PageSpeed/Monitors/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/PageSpeed/Monitors/index.jsx b/Client/src/Pages/PageSpeed/Monitors/index.jsx index 9376d07ce..228e522ff 100644 --- a/Client/src/Pages/PageSpeed/Monitors/index.jsx +++ b/Client/src/Pages/PageSpeed/Monitors/index.jsx @@ -15,7 +15,7 @@ import useMonitorsFetch from "./Hooks/useMonitorsFetch"; // Constants const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; -const NewPageSpeed = () => { +const PageSpeed = () => { const theme = useTheme(); const isAdmin = useIsAdmin(); const { user, authToken } = useSelector((state) => state.auth); @@ -63,4 +63,4 @@ const NewPageSpeed = () => { ); }; -export default NewPageSpeed; +export default PageSpeed;

bluewave-labs%2Fcheckmate | Trendshift