From 859cdc40b918282655a537b8065269a5dbce6d52 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 17 Nov 2021 17:51:46 +0100 Subject: [PATCH 01/55] Update README --- README.md | 6 ++++++ img/screenshot.png | Bin 0 -> 57870 bytes 2 files changed, 6 insertions(+) create mode 100644 img/screenshot.png diff --git a/README.md b/README.md index 08f496b..dbf8609 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Monito multi-monitoring checker +Monito is a simple tool to query monitoring servers and display +results in the Gnome-Shell panel. + +![Screenshot](img/screenshot.png "Screenshot") + + ## Manual Installation If you install from sources/scratch, put the sources into the diff --git a/img/screenshot.png b/img/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..95cd25a0627e536f4449f74bc6f21fc24761ed9a GIT binary patch literal 57870 zcmW)m2RK`A8^(`K&CpV{N2yx1_o%(KTC4UbwO5H9lv+j6+M~3pN>OT)ST$<3wI#%! zmDnL9`TV~t^SsIRp5&bKJkRgGpEvoDi7xGpJ2wCTpw-vYG6Mjjo$L2rl*HH1h>!b~ z*Ds{*4~-uJ!1oO5D<`t+ZEhDmGh+Y<7XW~mcmTj&55;T)K=14vq!p z0LK?P4}bU!>S&XKw0RlGSsC?WM)mJjJ?#Rc6(Z`Iq9HZ^%{CojUYA8|vk`d|YYCRe z6g6V(Bfg=rwdmf{G}cJeIpVNwEog8n9b!&l?|-=E7v3`nPQR$}3s+NJU2_lUm<0jf zqyMlt1S2<3Y#_68xS6AX4K;@|KQ}>@h`Dd|j#rb#8b6wi0{SjZ55LCw76)pRk zu7(w-C`4E4xF!nsWOF61iMzAXkcxQ=qx^jl(ozR`NOejt4u}#!=MYd+=ZAm4W{;=h z#M!_urIbW6E^h&?`)VquQdquA!ucrx7=_Jnky)jAatDB8~H8iM$*R z7#Ny`b>)GkiKadz0tj`N$n z?gw9(z2Oz#f7m6UH{c3Jr*J6Tz4gkA6k437iZV`=VJ+_Y)B4otC&$+jTDtVx7HllC z&ppbEBF5+4i^D#nU($lZVy~)|5xKnYm81d=*_ zJGt~vNabw^F1QZ6pAmJi=x}<=p}mz>3kXH9s9^cfgh9E^vyBnQ7Qc{6OH0e8D?vYO zs#?_Kua|-BK?mrbNAg;Z4adcFE*I^VgtI}_b5sQKvSdw(#oizP-}&)Fcw;C z_w302e+#cUx#$rCN`2XFZSdqub`sX?|m;7$^5q)9J=}vB16*2c9oQcF=NBY zAnpYo)()n7DXl7zD#V6#+|lmteStcwP?n{wR4&3CEPMpMTAXN)U8YdmK2BE=FQ;&1 zRP*z6AgsIpb2~bJTh|AXdldQzY!cZB{|SRbXiR?`G@o)Dw4$R?@+%96YakQZ6&~nG zK&O{lxZoRxrHX8>422N0#sK(mGI)o*^sui*48GB0*BLh$^#iI|atC}Rqk#Hk75b}{ zgqzbH)s9yxIUa+Y=D>*5wL-^Rl#gfNzDo5FNM}S#<9H!LBZPkq!R&Vs%hWCl$JCUPmal?8J^wRPOBH z>$w*319+4!PhDy6cf(S`hckVL5e&4FL-DEVc_xY$;@E-+|I5@}oHQCq6@J8o=-^`d zL2M+j4++Bul^&cAmP9XiC#oI&VIlmjv%}-*8-6E>LuS#?L;8Z|i*j8xPZyW7ud`h% zrTCpFwR4Pf1!XBFkA@}z$+;|efbPcQ6usWgNfd1 zF@>Ff55!-DkI5`Xh^t+t!oJEOuUMrK_0#@-YZ0Q~rXdn7>z~dpUF0c}x34 zbsgNo(Px}C9GXbirj}x$qaFL452X}s5E7o%`s39(UB_-{6pR2PYf-m6 z@c8meBu2vFe8>S;DvlGMfYd6rMm&!C_mS(g41zzn6eD!^ah?5UiQEeEh2ajo>@fo_ zgKImc5r54sA-mU?_+fowG@d?+K)nRMpb(3gxNK$}T4bM!3>m1yrHJRNZsn_!0+A?% z>-)|z*AINQT3u=Trt8-8JU=j-P@Gc2H?>1PwN@w;1gZP>O$>JG1-n`uRK;eZqmY+s z0n^hN5&u3qT>e5{rPPHTHO<;l0H$GoXCbRhmXOu2)#A7-3c>+>bjDY^vl7fw@PxCx<* z9&$F*j(%`%Gl~_$+;6!nuv(>bET+~diVBPLtw7&b2rF9aD8cwloDT>xi3dg~f00Q~i1XnZ(d4`e%ZNJRLtbDA=l}d7 zpsyg9B{t>_Uc=#G#Yh8ji+$Ss9VT1M_?kNvxZUMj(GHl?RnIp|J;I96NhYE3N^ z7bV^~5j76OSi6^m9cp7DaBR^8!D#Fj(Xuh@62^I%r)w;jJs%nE&KwL)5WpN<2#y&0 zouol@3jX#89^%D`Jtw0eA0sCE5)-X3C0A7}@D;GPqGsFL)3hll7Z&<*B}#b<4q3^? zV(e7I{(ZzmZM7UPjxP|(OFIv}N+a(Z^Ow9kwHNWZ8r=vAk>9$-weZv~c;#+xiDTKt z;gJ}{ggnj%aw>>CuknMRFPC6zW5|THYUt{SZ`Xb^jBp9|(aQyDp{1aQE9tA z*jvb3@clxx*ORODj=Ve3H40N;sTW*j!a4s$1f1!a32`vvs&bUoXUni z;bISZWynd$LPpL<$~ss*@7GmXbux`OViIrPj;`dA2Y|}rW z^PN7C$(o*vX6%mOCn398huWQjG{-CJ&3 z2U8|Ep9vwJIDKEZG#9;8`gjxK(GJcSh<3y>;D zh2kVJD7#!II4WvfuBDW)Jlw~r2$}6Ltw>g`<+G-k0{b19_5`m8egCB7xVqwth&(Q1 z^Xt4|u|LUn!1Y&-Z!I1|lawBnB~o|Is-O=GEIUuAxd^vLs6P~fDbCQai3<$$XsvW^ z<5Y~r5rXgIQo~2hBVo@Kiw8PbY$JD?k;XzQdpo8V<6F^tY!JeAgtJ74U3~L9$JXHo zDx>?_wn~EL)w(d1_9v^?d-jOjQ-dr@_^k+BMq@fkFB`?LzEju+{@LIolp$s76S2dwO>3Inv1;sl2}p^B@i z4BTwdh?fH)Vx%(aHzc&%4|ZZXp}057bxw$06^5Sa50+9n?Hm3%l#+jU!Cged=UKn_ z0q5>(E}aog3amp~Ywq~QT;)QT_RKgBf7{hkV8&<&``gWZLEYa>J&!pbQL&C^HB(;LUz2NQg{>BQZI_~i^JHW?|82akv*qJBud0AKa>`jY^yocKhin3>lK zt+Jiq``HYIZWkVx=gX|XqnsSvA4`}j{srtHvvSkjKd3DqS@EQp_*K-uVU+hqYH2-w zhKK>bD(csT=|UZ+pio2pcLF;N8o5&P2)JbL))rmt_ntBBKkV-Qv#T_i7u^m z6XlFPFhT+k?xk152V`6vtA+Q`M&cZDYj;rKS^-a{Egt#WK&yOz~Lx<5CoD}-u#n68j}%u;?g0_#C0)u3plu- z5BoiMT?=(pvER=pMor3W^ddNEfLh(C5OW-^2kk2sK7EooYb+M!cO0X(ECt=Xnf{K; zF0mYVE7lfzxQcz9OMG2}=fTrj*fZ@RK__U2X5>;Dtf6qB-OgDOE_y8bWCHB?4tgQJ zL<~%XW1nz^?`WD_3B&DH+O~>t3zqoJwk>atOmtWem|c18M6(4^VD>B;(@#Hui%S(> zT(xxw9o7_YJvT!-=`M7*Pt;wlP80=i=A)tiRBY029D`LcHZ!wwQ2g*j<`@u81WXaIS${f98`e&o%8Z|#ugd5fU6_Ic^W#h2SdObMn_EER6jqF znI-OSEIFLwIxj(o|MVkZWgP~`bP#gZJKt`-?l|qBh&Qx|Ar+u-Bt+p>-LZEk6wR4q zCzP$~UL3!6ZQ&vZ6!(@zNo zsU-vsWccNNB4O4m8k}o+1RYU3Xof{?b)kbZkWpjk$jcVhy)B0mCbg?qNRmp_hp3G8 zNlsc|1Dh*~@~L0ES4q?)-$qe-Hn)iv_Y(`>5Ncekc*%9n4b-UY>|r9|jguD``->xS zm1E<&halkX@p#v9<-ppr*)b_F0Y4dys)StmA&)B2SUBto4Lj&1p*SM}r0fpMz=Sz^ zLifbgdd3wFj=V~T;T!$J&L-v3@KkN<9U|qiPb{`%byoZ!Bc);@LcNzugr4gZmz)%R zmY}9sY-F-qWEr{LAguaw9(lFFRT_a(Sc|%zb_9B)7QKV)I=0g~nG#exEZD0hiFx(P z@?}Hp*bW6<=V6n$T3Oa2hi-Z#hW=`M4SG_YLDtW8^kG8j($^g~21f?;rO7FD{|DT_ z@(6T2V8qW3(q{}Dbignaz(Iv+DB_5LUHO;>XhpT}5iNhMR@2s^p!)|oYlH^)_?%is z?^hI_G1|t>F1I#c&S@~nf2Ik{u9#E)7RM4WQl<$xooQK&+@2AAO&Eh*%+^KW*tFNZ zV4K27LM%P>m-iyKCw>{mG(GEK}UiNkH z_cZFg-QhED_mrrcq-CM$LX{j)&>2{rGst&Q*EGM}+)^f(;{T0p6-Bk^D*r30!tAxxRNG7V;*UHz6Y%1#=^(y((B+H zgx`>L>%|QRN!Y)?D4auy+C`8(9SKm1HxHZ7u2WfHt^-V8bR{K^>yEva;&Plv z%M+IehkcI#;C!zIip$Z2zkM>Hg7F4gkJe;dueJwkNn?V>p=`WGa-FxR!I54UZ5)a( zg#pAdCs;S;cw9+$`ss)iY)2>;MtIeN2;3%g?#Q4o5?k=UAQ68ggZ{~5?iK3xwJf}# zh$&EHt70&;Wri$Bl{PX+R?P=ybnQjcM{YaLA10_>rix$96Po8}UrQ-(Q*+v|)MIg+ ztq-dF!uH^3On0Ki$NgwV<-$WN80Sn~9c03{r_Zu)CFAl{2|WLaMg!>LN%X>&7J$! zGKLJEUvW4`Z(ZSRh(6fjsyZCo!KY%x%9|1nq5nbm;(yTXaDGg8aOrpM;BfMP=(Y{F za<;-PW1?qUqK~CG+ehn<*6wTdR`upY%8}Xfm9Rh%{(Z*ukhuyjT(@lXPQEE*SE|&$ zWocyT7+R@{v&qOm7A%s!wm%VDOV=!vNVM$h&QUb61^gDv)CuvpMaMoz_?in_zcwod zN28A_AuH>jQJb^?pcC#p9wuALH3vg-(0NB!qxWncEORFLPmZ-jF25pNRX7}-XE+3( zvZ!oQBadcURMyx^8`|5C_F#ufFnqPRJu=cICN1oLb`nGRuMIYwM;hIakL&Pal zsYg`gApum(5orMlTjM-*^X^xgzSYa0V=26816(2vfp2bqS^j(}_89xI9IM_jx&- z(O+L&+XMfuVJ&iH0>4`Aa5*3z*2@xgxW@Iw2Xipm(gbQ4yS<_(#|f1r{!NLJ&+4j{ z2vA2zN*2n-tqflVyhae!W3yAAq|t2_SG`g@^Qv~ACEd*QG9BafMT2@VU<4aK8&*TiP2dO*ZN)O{+4kk9`CJV3W+pRVKkNW`u2 zTN`M?axok7!^tA1mwL4n1D#a?NgV9$t$m{wOY%x>sqI2xMcvQ)%}XPXQxy$9iL-o&@2)5>YH8j*+a!z@l^@5H|O6T zcucWmTPtGMX)h^#-hhk7BB8_7v->K`kI1?6l<$EWsYT?eck3k!5s4pHw^ZqY?g*t5 z-Ad0-0vXY+;oWy}YF)FwUFN8;j3#%rDD0drN%so!T(s*VN6GJN?x0U9YV-4RVy=r9 z8wXeYri93G>Lq@u#R>_fZ~CA^CnAu9gU>`U5s0q`^_6aYM>ej_RdAWV7G_MPrHd(2b zJOP{iMUTr3{ZrfFv^EQIZ!sKcN!h#nY$pN;BWGNJ$k(?5(MS1(hD%BRHLeU430rb1 z?eL|)kZt|E@nxJTaC3=pK8CS68iX9dVGTz!5o9?_e_lF_*vtZR>LcyjPIr}0*+ z_SOe{z{LhvC>#kr{+1jDIZ#2z zKlkn@@;~&w3UK;x1I`$fSQtLa0@*kkp$|KN$RCfsay#o&!%M-ZR#EGo@^93@|7kz$ zE%C0)Md4#X>}c0jOSJK{Jq9aR3|>+I{MSPAKQR@cS#VPsNkfCR=jW59IF3YLl~}#; zW7wL_1Qc$47o#;#C`M%}V&~>F{bHE&B*j@KYR92~L@#~fN zZBslu!0gv`w!zi0x|U%Z?gd|im*9-Ehu3o<`*Qy`I70WnwW#dto7(1^pd(huV7Mbo z4)^9XdeehEGh3tvEt21xxV34~I+zZW`;-vb7CcKg=@T29Asc?u)Hi>YeW%b^*M4xs zVb$I4Mg)p38wZdc+bj>soL9AU{~K4dR${*nurdd5RQQ$4I-X^r4E`%oRT+XqUvlV% zz>cEkjr%s@EReFxi$7>gT77X-B9}@YvtLy36v|w?s^`Ix2g5(M9?q3e45@bfO1HPK zy)Z9z2)oqmK(VYTS#04htthS|YL8nl7IcKldYj>7wpsCU$X(w6RM1kZ9L}Ny= z4v?e4=xsRkf)#RANsx=)?tj(%E_aT2igw@cLEg^?Ptk7@lt22ZKs$o1uC&x;R^6sY zD(E_FBpF9)G4BbH^RWEr_bm2-I~LrR=Xyz8d^*i(gV6XuY(U4#A{jjDXQJ38c)Bhy zJGcGku8-hym@?IeY|n|%-So9B{uXG&VTHdWOGc94Wwl!VM2C;(s@%kqi_N0%JpcIh z4a2`?D?Qk7XxY#-h9kna zo2$VBFMVfe*%%H>@hf$vV~PpJ2M4lhk)3`ep~NB|$2+XwOVSJrWJIWoI$k7WUzjd_ z^c={@tXeoZA%DilIZ5Vr0lM52+p!5r-44C*gCTn<^Gdu+(F>UQT$8myn?UHEz+tIU zh^)Fzq6-7zq3td+aR=M)Y}{#wfA8<=`I*g?VL6<&{YjGlv1p6!(XwvILEDANAMTRn zJNR`|WM}I0AH&W6blP3vb$xWbq%ti;O46X8#h0clwb4adB!~DveM+f4H;}k5z!^P2eOm)H|;BEVZO~jQiX;<9#yX~UG z60gi?S(;gqN&z_N@mJ;i#^ITu^eG>Q7jRm;B{?r%GRtVn*iPR?RSf{H=f{q&GS4M* zn)Kti+=sx&SC33u5N}+ve>I9fTk(1P{;<0$Qpw@++-J?S6~BU1>JD7B?lfd>M)$TH zx#XOfIP}=r)|Y;%8)NQkZzQzZUOc9q*AGz~Si8E>E5O)Ls!n$)-_o$}v+sGzG3o1H zO}z`>e(Wb?)Bl{3($r0;cyyJ~fw+v>;-)O?KmQ*T8cBZlf7pKQig#hi$hN)>J)uMq z_pLkBd03S+3|@4b$;Q24UDg2jwPY5hIa~8L!URJRrLQbPWSI&>CheN>3"moDyh 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 TQ`#Cn00000NkvXXu0mjf_h;O* literal 0 HcmV?d00001 From 168b509b2282a2658d0f803d62a75140235632d0 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 17 Nov 2021 20:16:53 +0100 Subject: [PATCH 02/55] Some UI refinements --- extension.js | 36 +++++++++++++++++++++++++++++++++++- prefs.js | 7 +++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/extension.js b/extension.js index c74806a..3d5b02a 100644 --- a/extension.js +++ b/extension.js @@ -212,10 +212,31 @@ class Indicator extends PanelMenu.Button { this.setupTimeout ( ); } + createHeaderBin ( status, text, col ) { + let _widths = [ 300, 300, 200, 50, 600 ]; + let _bin = new St.Bin({ + style_class: 'monito-service-' + status, + width: _widths[col], + x_expand: ( col == 4 ? true : false ), + child: new St.Button ( { + x_align: Clutter.ActorAlign.FILL, + y_align: Clutter.ActorAlign.CENTER, + reactive: true, + can_focus: true, + track_hover: true, + accessible_name: text, + style_class: 'button', + label: 'Foo', + }) + }); + return _bin; + } + createBin ( status, text, col ) { let _widths = [ 300, 300, 200, 50, 600 ]; let _bin = new St.Bin({ style_class: 'monito-service-' + status, + track_hover: true, width: _widths[col], x_expand: ( col == 4 ? true : false ), child: new St.Label({ style_class: 'monito-label', text: text }) @@ -228,6 +249,19 @@ class Indicator extends PanelMenu.Button { this._box.remove_all_children(); + let headerBox = new St.BoxLayout({ + style_class: 'monito-service-line', + hover: true, + x_expand: true + }); + this._box.add_child(headerBox); + + headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 0 ) ); + headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 1 ) ); + headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 2 ) ); + headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 3 ) ); + headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 4 ) ); + for ( let i = 0 ; i < serverLogic.status.service_status.length ; i ++ ) { _status [ serverLogic.status.service_status[i].status ] ++; @@ -285,7 +319,7 @@ class Indicator extends PanelMenu.Button { can_focus: true, track_hover: true, accessible_name: text, - style_class: 'button' + style_class: 'button', }); button.child = new St.Icon({ diff --git a/prefs.js b/prefs.js index 919966e..4c8fc44 100644 --- a/prefs.js +++ b/prefs.js @@ -189,6 +189,13 @@ function createAccountWidgets ( isActive ) this.prefWidgets = { }; + this._accountsWidget.attach ( new Gtk.Label({ + label: '' + _('Accounts settings') + '', + visible: true, + halign: Gtk.Align.START, + use_markup: true, + }), 0, 0, 2, 1 ); + let y = 1; for ( var prefEntry of prefs ) { From 3f50ae001636bf5481cea8ee0f9f7813b3f173ca Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 17 Nov 2021 21:01:51 +0100 Subject: [PATCH 03/55] Cosmetic changes --- extension.js | 14 +++++++------- servers/icinga.js | 15 +++++++++++---- servers/icinga2.js | 4 ++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/extension.js b/extension.js index c74806a..9a17261 100644 --- a/extension.js +++ b/extension.js @@ -28,16 +28,16 @@ let _httpSession; let _status; let _ok_text; -const Gettext = imports.gettext.domain(GETTEXT_DOMAIN); -const _ = Gettext.gettext; -const Lang = imports.lang; - const ExtensionUtils = imports.misc.extensionUtils; -const Me = ExtensionUtils.getCurrentExtension(); +const Lang = imports.lang; const Main = imports.ui.main; +const Mainloop = imports.mainloop; +const Me = ExtensionUtils.getCurrentExtension(); const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; -const Mainloop = imports.mainloop; + +const Gettext = imports.gettext.domain(GETTEXT_DOMAIN); +const _ = Gettext.gettext; const { GObject, St, Clutter, Gio } = imports.gi; @@ -46,9 +46,9 @@ const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito.account"; const SETTINGS_SCHEMA_ACCOUNT_PATH = "/org/gnome/shell/extensions/monito/account"; const Convenience = Me.imports.convenience; -const Preferences = Me.imports.prefs; const Icinga = Me.imports.servers.icinga.Icinga; const Icinga2 = Me.imports.servers.icinga2.Icinga2; +const Preferences = Me.imports.prefs; let settings = Convenience.getSettings(SETTINGS_SCHEMA); let account_settings = [ ]; diff --git a/servers/icinga.js b/servers/icinga.js index fe59257..5bdb4d9 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -23,10 +23,10 @@ const { Soup } = imports.gi; const ExtensionUtils = imports.misc.extensionUtils; -const Me = ExtensionUtils.getCurrentExtension(); -const Preferences = Me.imports.prefs; const Lang = imports.lang; const Main = imports.ui.main; +const Me = ExtensionUtils.getCurrentExtension(); +const Preferences = Me.imports.prefs; let _httpSession; @@ -65,9 +65,16 @@ class Icinga { _httpSession.queue_message(message, Lang.bind (this, function(_httpSession, message) { -// log ( '>>> ' + message.response_body.data ); - let json = JSON.parse(message.response_body.data); + log ( '>>> ' + message.status_code ); + + if ( message.status_code != Soup.Status.OK ) + { + log ( '>>> error: ' + message.reason_phrase ); + Main.notify ( 'Monito: ' + name, message.reason_phrase ); + return; + } + let json = JSON.parse(message.response_body.data); this.status = json.status; extension.refreshUI ( this ); diff --git a/servers/icinga2.js b/servers/icinga2.js index c00b979..cd2c3f9 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -23,10 +23,10 @@ const { Soup } = imports.gi; const ExtensionUtils = imports.misc.extensionUtils; -const Me = ExtensionUtils.getCurrentExtension(); -const Preferences = Me.imports.prefs; const Lang = imports.lang; const Main = imports.ui.main; +const Me = ExtensionUtils.getCurrentExtension(); +const Preferences = Me.imports.prefs; let _httpSession; From 33522a08f9d6ca90ae0e2c9bbb312d5dde47e567 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Thu, 18 Nov 2021 01:32:48 +0100 Subject: [PATCH 04/55] Some refactoring --- servers/icinga2.js | 118 +++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/servers/icinga2.js b/servers/icinga2.js index cd2c3f9..0be7a6e 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -38,74 +38,76 @@ class Icinga2 { } refresh ( extension ) { - let _settings = Preferences.getAccountSettings ( this.server ); + this.extension = extension; + this.settings = Preferences.getAccountSettings ( this.server ); log ( 'monito server >>> ' + this.server ); - let type = _settings.get_string ( "type" ); - let username = _settings.get_string ( "username" ); - let name = _settings.get_string ( "name" ); - let password = _settings.get_string ( "password" ); - let urlcgi = _settings.get_string ( "urlcgi" ); + this.type = this.settings.get_string ( "type" ); + this.username = this.settings.get_string ( "username" ); + this.name = this.settings.get_string ( "name" ); + this.password = this.settings.get_string ( "password" ); + this.urlcgi = this.settings.get_string ( "urlcgi" ); - log ( 'monito name >>> ' + name ); - log ( 'monito type >>> ' + type ); - log ( 'monito username >>> ' + username ); - log ( 'monito urlcgi >>> ' + urlcgi ); + log ( 'monito name >>> ' + this.name ); + log ( 'monito type >>> ' + this.type ); + log ( 'monito username >>> ' + this.username ); + log ( 'monito urlcgi >>> ' + this.urlcgi ); -// urlcgi = urlcgi.replace ( /^(https?:\/\/)/, '$1' + username + ':' + password + '@' ); + this.prepareHttp ( ); + } + prepareHttp ( ) + { if (_httpSession === undefined) { _httpSession = new Soup.Session(); _httpSession.user_agent = Me.metadata.uuid; } - try { - - let message = Soup.form_request_new_from_hash ( 'GET', urlcgi, { 'format': 'json' } ); - message.request_headers.append ( 'Accept', 'application/json' ); - - let auth = new Soup.AuthBasic() - auth.authenticate ( username, password ); - message.request_headers.append ( "Authorization", auth.get_authorization ( message ) ); - - - _httpSession.queue_message(message, Lang.bind - (this, function(_httpSession, message) - { - // TODO format this in the manner of icinga1 -// log ( '>>> ' + message.response_body.data ); - let json = JSON.parse(message.response_body.data); - let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; - - this.status = { }; - this.status.service_status = [ ]; - - for ( var entry of json ) - { -// if ( entry.status != 0 ) -// log ( JSON.stringify(entry, null, 2) ); - - this.status.service_status.push ( { - status: _statuses [ entry.service_state ], - host_name: entry.host_name, - service_display_name: entry.service_display_name, - attempts: entry.service_attempt, - status_information: entry.service_output, - last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), - } ); - } - - extension.refreshUI ( this ); - } - ) - ); - return true; - } - catch (e) { - Main.notify(_('Zbeu!')); - log(e); - return false; - } + let message = Soup.form_request_new_from_hash ( 'GET', this.urlcgi, { 'format': 'json' } ); + message.request_headers.append ( 'Accept', 'application/json' ); + + let auth = new Soup.AuthBasic() + auth.authenticate ( this.username, this.password ); + message.request_headers.append ( "Authorization", auth.get_authorization ( message ) ); + + _httpSession.queue_message ( message, Lang.bind (this, this.handleMessage ) ); } + handleMessage ( _httpSession, message ) + { + log ( '>>> ' + message.status_code ); + + if ( message.status_code != Soup.Status.OK ) + { + log ( '>>> error: ' + message.reason_phrase ); + Main.notifyError ( 'Monito: ' + this.name, + 'URL: ' + this.urlcgi + "\n" + + message.status_code + ': ' + message.reason_phrase ); + return; + } + + let json = JSON.parse(message.response_body.data); + let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; + + this.status = { }; + this.status.service_status = [ ]; + + for ( var entry of json ) + { + // if ( entry.status != 0 ) + // log ( JSON.stringify(entry, null, 2) ); + + this.status.service_status.push ( { + status: _statuses [ entry.service_state ], + host_name: entry.host_name, + service_display_name: entry.service_display_name, + attempts: entry.service_attempt, + status_information: entry.service_output, + last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), + } ); + } + + this.extension.refreshUI ( this ); + return true; + } } From 49f4cbebb497d3a565b054418a1f7dc275fc5cfd Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Thu, 18 Nov 2021 23:08:49 +0100 Subject: [PATCH 05/55] Implement real multi-indicators extension + some cleanup --- extension.js | 131 +++++++++++++++++++++++---------------------- servers/icinga.js | 2 - servers/icinga2.js | 5 -- 3 files changed, 68 insertions(+), 70 deletions(-) diff --git a/extension.js b/extension.js index c9da8f7..11ba4e2 100644 --- a/extension.js +++ b/extension.js @@ -56,9 +56,11 @@ let account_settings = [ ]; const Indicator = GObject.registerClass( class Indicator extends PanelMenu.Button { - _init() { + _init(server) { super._init(0.0, _('Monito Checker')); + this.server = server; + this.initStatus ( ); this.namesBoxes = { }; @@ -67,53 +69,48 @@ class Indicator extends PanelMenu.Button { this.unknownBoxes = { }; this.initUI ( ); - - settings.connect("changed::servers", Lang.bind ( this, function(stgs, key) { - monitoLog ( this ); - this.remove_all_children ( ); - this.initUI ( ); - } ) ); } initUI ( ) { let box = new St.BoxLayout ( { } ); this.add_child(box); - for ( let _server of Preferences.getServersList() ) - { - monitoLog ( '> Server ' + _server ); - account_settings [ _server ] = Preferences.getAccountSettings ( _server ); - let _account_settings = account_settings [ _server ]; - - let serverBox = new St.BoxLayout ( { style_class: 'monito-serverbox' } ); - box.add_child(serverBox); - - let name_box = new St.BoxLayout( { style_class: 'monito-namebox' } ); - this.namesBoxes [ _server ] = new St.Label ( { text: _account_settings.get_string ( 'name' ) } ); - name_box.add_child ( this.namesBoxes [ _server ] ); - serverBox.add_child(name_box); - _account_settings.bind ( 'name', this.namesBoxes [ _server ], 'text', Gio.SettingsBindFlags.GET ); - - let warning_box = new St.BoxLayout({ style_class: 'monito-warning-box monito-box' }); - warning_box.set_style ( 'background-color: ' + _account_settings.get_string ( 'warning-color' ) ); - _account_settings.connect("changed::warning-color", Lang.bind ( { widget: warning_box }, setColor ) ); - - this.warningBoxes [ _server ] = new St.Label({ text: String(_status['WARNING']) }) - warning_box.add_child ( this.warningBoxes [ _server ] ); - serverBox.add_child(warning_box); - - let critical_box = new St.BoxLayout({ style_class: 'monito-critical-box monito-box' }); - critical_box.set_style ( 'background-color: ' + _account_settings.get_string ( 'critical-color' ) ); - _account_settings.connect("changed::critical-color", Lang.bind ( { widget: critical_box }, setColor ) ); - - this.criticalBoxes [ _server ] = new St.Label({ text: String(_status['CRITICAL']) }) - critical_box.add_child ( this.criticalBoxes [ _server ] ); - serverBox.add_child(critical_box); - - } - + monitoLog ( '> Server ' + this.server ); + account_settings [ this.server ] = Preferences.getAccountSettings ( this.server ); + let _account_settings = account_settings [ this.server ]; + + let serverBox = new St.BoxLayout ( { style_class: 'monito-serverbox' } ); + box.add_child(serverBox); + + let name_box = new St.BoxLayout( { style_class: 'monito-namebox' } ); + this.namesBoxes [ this.server ] = new St.Label ( { text: _account_settings.get_string ( 'name' ) } ); + name_box.add_child ( this.namesBoxes [ this.server ] ); + serverBox.add_child(name_box); + _account_settings.bind ( 'name', this.namesBoxes [ this.server ], 'text', Gio.SettingsBindFlags.GET ); + + this.namesBoxes [ this.server ].connect ( 'button-press-event', Lang.bind ( this, function ( ) { this.setMenu ( this.menu ); } ) ); + + + let warning_box = new St.BoxLayout({ style_class: 'monito-warning-box monito-box' }); + warning_box.set_style ( 'background-color: ' + _account_settings.get_string ( 'warning-color' ) ); + _account_settings.connect("changed::warning-color", Lang.bind ( { widget: warning_box }, setColor ) ); + + this.warningBoxes [ this.server ] = new St.Label({ text: String(_status['WARNING']) }) + warning_box.add_child ( this.warningBoxes [ this.server ] ); + serverBox.add_child(warning_box); + + let critical_box = new St.BoxLayout({ style_class: 'monito-critical-box monito-box' }); + critical_box.set_style ( 'background-color: ' + _account_settings.get_string ( 'critical-color' ) ); + _account_settings.connect("changed::critical-color", Lang.bind ( { widget: critical_box }, setColor ) ); + + this.criticalBoxes [ this.server ] = new St.Label({ text: String(_status['CRITICAL']) }) + critical_box.add_child ( this.criticalBoxes [ this.server ] ); + serverBox.add_child(critical_box); + box.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM)); + this.menu_new = new PopupMenu.PopupMenu(this, Clutter.ActorAlign.START, St.Side.TOP, 0); + // Menu this._buttonMenu = new PopupMenu.PopupBaseMenuItem({ reactive: false, @@ -121,12 +118,6 @@ class Indicator extends PanelMenu.Button { }); this.menu.addMenuItem(this._buttonMenu); -// let item = new PopupMenu.PopupMenuItem(_('Reload')); -// item.connect('activate', () => { -// this.updateStatus ( ); -// }); -// this.menu.addMenuItem(item); - let _path = Me.path + '/img/monito.png'; let _icon = new St.Icon({ gicon: Gio.icon_new_for_string(_path), @@ -156,17 +147,22 @@ class Indicator extends PanelMenu.Button { x_expand: true }); _intermediate.actor.add_actor(this._box); + this.menu_orig = this.menu; this.updateStatus ( ); this.setupTimeout ( ); } + cancelTimeout ( ) + { + if (this.timeout) + Mainloop.source_remove(this.timeout); + } + setupTimeout ( ) { monitoLog ( 'Setting up timeout of ' + settings.get_int ( "poll-delay" ) + ' secs' ); - if (this.timeout) { - Mainloop.source_remove(this.timeout); - } + this.cancelTimeout ( ); this.timeout = Mainloop.timeout_add ( settings.get_int ( "poll-delay" ) * 1000, Lang.bind(this, this.updateStatus ) ); } @@ -181,7 +177,7 @@ class Indicator extends PanelMenu.Button { { for ( let _server of Preferences.getServersList() ) { - let _account_settings = Preferences.getAccountSettings ( _server ); + let _account_settings = Preferences.getAccountSettings ( this.server ); let type = _account_settings.get_string("type"); let username = _account_settings.get_string("username"); @@ -196,14 +192,14 @@ class Indicator extends PanelMenu.Button { { let _serverLogic; if ( type == 'Icinga' ) - _serverLogic = new Icinga ( _server ); + _serverLogic = new Icinga ( this.server ); else if ( type == 'Icinga2' ) - _serverLogic = new Icinga2 ( _server ); + _serverLogic = new Icinga2 ( this.server ); if ( ! _serverLogic.refresh ( this ) ) { - this.warningBoxes[_server].set_text ( '…' ); - this.criticalBoxes[_server].set_text ( '…' ); + this.warningBoxes[this.server].set_text ( '…' ); + this.criticalBoxes[this.server].set_text ( '…' ); // TODO: Add display of error if any } } @@ -294,7 +290,7 @@ class Indicator extends PanelMenu.Button { this.warningBoxes[serverLogic.server].set_text ( String(_status.WARNING) ); this.criticalBoxes[serverLogic.server].set_text ( String(_status.CRITICAL) ); - + return; } @@ -341,23 +337,32 @@ class Extension { constructor(uuid) { this._uuid = uuid; + settings.connect("changed::servers", Lang.bind ( this, function(stgs, key) { + this.disable ( ); + this.enable ( ); + } ) ); + ExtensionUtils.initTranslations(GETTEXT_DOMAIN); } enable() { - this._indicator = new Indicator(); + this._indicators = { }; - Main.panel.addToStatusArea(this._uuid, this._indicator); + for ( let _server of Preferences.getServersList() ) + { + this._indicators [ _server ] = new Indicator(_server); + Main.panel.addToStatusArea ( '%s-%d'.format ( this._uuid, _server), this._indicators [ _server ] ); + } } disable() { - this._indicator.destroy(); - this._indicator = null; + for ( var i of Object.keys(this._indicators) ) + { + this._indicators[i].cancelTimeout(); + this._indicators[i].destroy(); + } - if (_httpSession !== undefined) - _httpSession.abort(); - - _httpSession = undefined; + this._indicators = { }; } } diff --git a/servers/icinga.js b/servers/icinga.js index 5bdb4d9..45a5e02 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -65,8 +65,6 @@ class Icinga { _httpSession.queue_message(message, Lang.bind (this, function(_httpSession, message) { - log ( '>>> ' + message.status_code ); - if ( message.status_code != Soup.Status.OK ) { log ( '>>> error: ' + message.reason_phrase ); diff --git a/servers/icinga2.js b/servers/icinga2.js index 0be7a6e..964a573 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -74,8 +74,6 @@ class Icinga2 { handleMessage ( _httpSession, message ) { - log ( '>>> ' + message.status_code ); - if ( message.status_code != Soup.Status.OK ) { log ( '>>> error: ' + message.reason_phrase ); @@ -93,9 +91,6 @@ class Icinga2 { for ( var entry of json ) { - // if ( entry.status != 0 ) - // log ( JSON.stringify(entry, null, 2) ); - this.status.service_status.push ( { status: _statuses [ entry.service_state ], host_name: entry.host_name, From 25a3f7a6e87c8d0c90d434b47a36aeb79cc71edc Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Thu, 18 Nov 2021 23:16:59 +0100 Subject: [PATCH 06/55] Better error handling --- extension.js | 6 ++++++ servers/icinga.js | 16 ++++++++++------ servers/icinga2.js | 39 +++++++++++++++++++++------------------ stylesheet.css | 5 +++++ 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/extension.js b/extension.js index 11ba4e2..5ca0547 100644 --- a/extension.js +++ b/extension.js @@ -244,6 +244,12 @@ class Indicator extends PanelMenu.Button { this.initStatus ( ); // Specialize ! this._box.remove_all_children(); + + if ( serverLogic.error ) + { + this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: serverLogic.error } ) ); + return; + } let headerBox = new St.BoxLayout({ style_class: 'monito-service-line', diff --git a/servers/icinga.js b/servers/icinga.js index 45a5e02..ce056e4 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -69,17 +69,21 @@ class Icinga { { log ( '>>> error: ' + message.reason_phrase ); Main.notify ( 'Monito: ' + name, message.reason_phrase ); - return; + this.status = null; + this.error = message.reason_phrase; } - - let json = JSON.parse(message.response_body.data); - this.status = json.status; - + else + { + let json = JSON.parse(message.response_body.data); + this.status = json.status; + this.error = null; + } + extension.refreshUI ( this ); } ) ); - return true; + return this.status != null; } catch (e) { Main.notify(_('Zbeu!')); diff --git a/servers/icinga2.js b/servers/icinga2.js index 964a573..b78ea26 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -74,35 +74,38 @@ class Icinga2 { handleMessage ( _httpSession, message ) { + this.status = { }; + this.status.service_status = [ ]; + if ( message.status_code != Soup.Status.OK ) { log ( '>>> error: ' + message.reason_phrase ); Main.notifyError ( 'Monito: ' + this.name, 'URL: ' + this.urlcgi + "\n" + message.status_code + ': ' + message.reason_phrase ); - return; + this.error = message.reason_phrase; } - - let json = JSON.parse(message.response_body.data); - let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; - - this.status = { }; - this.status.service_status = [ ]; - - for ( var entry of json ) + else { - this.status.service_status.push ( { - status: _statuses [ entry.service_state ], - host_name: entry.host_name, - service_display_name: entry.service_display_name, - attempts: entry.service_attempt, - status_information: entry.service_output, - last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), - } ); + let json = JSON.parse(message.response_body.data); + let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; + + for ( var entry of json ) + { + this.status.service_status.push ( { + status: _statuses [ entry.service_state ], + host_name: entry.host_name, + service_display_name: entry.service_display_name, + attempts: entry.service_attempt, + status_information: entry.service_output, + last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), + } ); + } + this.error = null; } this.extension.refreshUI ( this ); - return true; + return this.status != { }; } } diff --git a/stylesheet.css b/stylesheet.css index 315a095..ee81b33 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -84,6 +84,11 @@ margin: 2px; } +.monito-network-error { + font-size: 150%; + font-weight: bold; +} + .monito-button-icon { padding: 0px; margin: 0px; From 378154a3b4e5176673b590020d5b978a757f8337 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 19 Nov 2021 11:20:59 +0100 Subject: [PATCH 07/55] Add schema for columns ordering and configuration --- ...org.gnome.shell.extensions.monito@drieu.org.gschema.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 989a94f..228d517 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -92,6 +92,13 @@ '#e496f5' + + ['service_description'] + + + + [0] + From 0ab8b79ae150bb182565337c8eef698a33bbea1a Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 19 Nov 2021 11:21:20 +0100 Subject: [PATCH 08/55] Implement better table view for services --- extension.js | 68 ++++++++++++++++++++++++++--------------------- servers/icinga.js | 2 +- stylesheet.css | 6 ++--- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/extension.js b/extension.js index 5ca0547..51baaef 100644 --- a/extension.js +++ b/extension.js @@ -53,6 +53,15 @@ const Preferences = Me.imports.prefs; let settings = Convenience.getSettings(SETTINGS_SCHEMA); let account_settings = [ ]; +const column_definitions = [ + { name: 'status', width: 50, expand: false, }, + { name: 'host_name', width: 300, expand: false, }, + { name: 'service_display_name', width: 300, expand: false, }, + { name: 'last_check', width: 200, expand: false, }, + { name: 'attempts', width: 50, expand: false, }, + { name: 'status_information', width: 600, expand: true, }, +]; + const Indicator = GObject.registerClass( class Indicator extends PanelMenu.Button { @@ -212,29 +221,29 @@ class Indicator extends PanelMenu.Button { let _widths = [ 300, 300, 200, 50, 600 ]; let _bin = new St.Bin({ style_class: 'monito-service-' + status, - width: _widths[col], - x_expand: ( col == 4 ? true : false ), + width: col.width, + x_expand: col.expand, child: new St.Button ( { - x_align: Clutter.ActorAlign.FILL, + x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, + width: col.width, reactive: true, can_focus: true, track_hover: true, accessible_name: text, style_class: 'button', - label: 'Foo', + label: col.name, }) }); return _bin; } createBin ( status, text, col ) { - let _widths = [ 300, 300, 200, 50, 600 ]; let _bin = new St.Bin({ style_class: 'monito-service-' + status, track_hover: true, - width: _widths[col], - x_expand: ( col == 4 ? true : false ), + width: col.width, + x_expand: col.expand, child: new St.Label({ style_class: 'monito-label', text: text }) }); return _bin; @@ -252,17 +261,23 @@ class Indicator extends PanelMenu.Button { } let headerBox = new St.BoxLayout({ - style_class: 'monito-service-line', hover: true, x_expand: true }); this._box.add_child(headerBox); + for ( let col of column_definitions ) + headerBox.add_child ( this.createHeaderBin ( '', col.name, col ) ); - headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 0 ) ); - headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 1 ) ); - headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 2 ) ); - headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 3 ) ); - headerBox.add_child ( this.createHeaderBin ( '', 'Foo', 4 ) ); + let scrollBox = new St.ScrollView ( { hscrollbar_policy: St.PolicyType.NEVER, + enable_mouse_scrolling: true, } ); + this._box.add_child(scrollBox); + + let tableBox = new St.BoxLayout({ + hover: true, + vertical: true, + x_expand: true, + }); + scrollBox.add_actor(tableBox); for ( let i = 0 ; i < serverLogic.status.service_status.length ; i ++ ) { @@ -271,26 +286,17 @@ class Indicator extends PanelMenu.Button { { let infoBox = new St.BoxLayout({ style_class: 'monito-service-line monito-service-line-' + serverLogic.status.service_status[i].status, - hover: true, - x_expand: true + track_hover: true, + x_expand: true, }); - this._box.add_child(infoBox); + tableBox.add_child(infoBox); - infoBox.add_child ( this.createBin ( serverLogic.status.service_status[i].status, - serverLogic.status.service_status[i].host_name, - 0 ) ); - infoBox.add_child ( this.createBin ( serverLogic.status.service_status[i].status, - serverLogic.status.service_status[i].service_display_name, - 1 ) ); - infoBox.add_child ( this.createBin ( serverLogic.status.service_status[i].status, - serverLogic.status.service_status[i].last_check, - 2) ); - infoBox.add_child ( this.createBin ( serverLogic.status.service_status[i].status, - serverLogic.status.service_status[i].attempts, - 3 ) ); - infoBox.add_child ( this.createBin ( serverLogic.status.service_status[i].status, - serverLogic.status.service_status[i].status_information, - 4 ) ); + for ( let col of column_definitions ) + { + infoBox.add_child ( this.createBin ( serverLogic.status.service_status[i].status, + serverLogic.status.service_status[i][col.name], + col ) ); + } } } diff --git a/servers/icinga.js b/servers/icinga.js index ce056e4..bf46a6f 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -83,7 +83,7 @@ class Icinga { } ) ); - return this.status != null; + return this.status !== null; } catch (e) { Main.notify(_('Zbeu!')); diff --git a/stylesheet.css b/stylesheet.css index ee81b33..f84c52c 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -30,7 +30,7 @@ } .monito-serverbox { - padding-left: 1em; + } .monito-box { @@ -43,10 +43,8 @@ } .monito-namebox { - line-height: 20px; - vertical-align: baseline; - height: 100%; margin-right: .5em; + vertical-align: middle; } .monito-critical-box, .monito-service-CRITICAL, .monito-service-line-CRITICAL { From 4a66c46042f563a9054a4f533ef6f00557f69485 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 19 Nov 2021 17:46:36 +0100 Subject: [PATCH 09/55] Implement column ordering + variable columns (need conf tool in the prfs window) --- extension.js | 176 +++++++++++------- prefs.js | 24 +++ ...ll.extensions.monito@drieu.org.gschema.xml | 6 +- servers/genericserver.js | 76 ++++++++ servers/icinga.js | 6 +- servers/icinga2.js | 8 +- 6 files changed, 216 insertions(+), 80 deletions(-) create mode 100644 servers/genericserver.js diff --git a/extension.js b/extension.js index 51baaef..5ab724f 100644 --- a/extension.js +++ b/extension.js @@ -46,6 +46,7 @@ const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito.account"; const SETTINGS_SCHEMA_ACCOUNT_PATH = "/org/gnome/shell/extensions/monito/account"; const Convenience = Me.imports.convenience; +const GenericServer = Me.imports.servers.genericserver.GenericServer; const Icinga = Me.imports.servers.icinga.Icinga; const Icinga2 = Me.imports.servers.icinga2.Icinga2; const Preferences = Me.imports.prefs; @@ -53,14 +54,14 @@ const Preferences = Me.imports.prefs; let settings = Convenience.getSettings(SETTINGS_SCHEMA); let account_settings = [ ]; -const column_definitions = [ - { name: 'status', width: 50, expand: false, }, - { name: 'host_name', width: 300, expand: false, }, - { name: 'service_display_name', width: 300, expand: false, }, - { name: 'last_check', width: 200, expand: false, }, - { name: 'attempts', width: 50, expand: false, }, - { name: 'status_information', width: 600, expand: true, }, -]; +const column_definitions = { + status: { label: _('Status'), width: 50, expand: false, }, + host_name: { label: _('Host name'), width: 300, expand: false, }, + service_display_name: { label: _('Service'), width: 300, expand: false, }, + last_check: { label: _('Last check'), width: 200, expand: false, }, + attempts: { label: _('Attempts'), width: 50, expand: false, }, + status_information: { label: _('Information'), width: 600, expand: true, }, +}; const Indicator = GObject.registerClass( @@ -77,6 +78,16 @@ class Indicator extends PanelMenu.Button { this.criticalBoxes = { }; this.unknownBoxes = { }; + this.sortIcons = { }; + account_settings [ server ] = Preferences.getAccountSettings ( this.server ); + this.account_settings = account_settings [ server ]; + + let type = this.account_settings.get_string ( "type" ); + if ( type == 'Icinga' ) + this.serverlogic = new Icinga ( this.server ); + else if ( type == 'Icinga2' ) + this.serverlogic = new Icinga2 ( this.server ); + this.initUI ( ); } @@ -85,32 +96,30 @@ class Indicator extends PanelMenu.Button { this.add_child(box); monitoLog ( '> Server ' + this.server ); - account_settings [ this.server ] = Preferences.getAccountSettings ( this.server ); - let _account_settings = account_settings [ this.server ]; let serverBox = new St.BoxLayout ( { style_class: 'monito-serverbox' } ); box.add_child(serverBox); let name_box = new St.BoxLayout( { style_class: 'monito-namebox' } ); - this.namesBoxes [ this.server ] = new St.Label ( { text: _account_settings.get_string ( 'name' ) } ); + this.namesBoxes [ this.server ] = new St.Label ( { text: this.account_settings.get_string ( 'name' ) } ); name_box.add_child ( this.namesBoxes [ this.server ] ); serverBox.add_child(name_box); - _account_settings.bind ( 'name', this.namesBoxes [ this.server ], 'text', Gio.SettingsBindFlags.GET ); + this.account_settings.bind ( 'name', this.namesBoxes [ this.server ], 'text', Gio.SettingsBindFlags.GET ); this.namesBoxes [ this.server ].connect ( 'button-press-event', Lang.bind ( this, function ( ) { this.setMenu ( this.menu ); } ) ); let warning_box = new St.BoxLayout({ style_class: 'monito-warning-box monito-box' }); - warning_box.set_style ( 'background-color: ' + _account_settings.get_string ( 'warning-color' ) ); - _account_settings.connect("changed::warning-color", Lang.bind ( { widget: warning_box }, setColor ) ); + warning_box.set_style ( 'background-color: ' + this.account_settings.get_string ( 'warning-color' ) ); + this.account_settings.connect("changed::warning-color", Lang.bind ( { widget: warning_box }, setColor ) ); this.warningBoxes [ this.server ] = new St.Label({ text: String(_status['WARNING']) }) warning_box.add_child ( this.warningBoxes [ this.server ] ); serverBox.add_child(warning_box); let critical_box = new St.BoxLayout({ style_class: 'monito-critical-box monito-box' }); - critical_box.set_style ( 'background-color: ' + _account_settings.get_string ( 'critical-color' ) ); - _account_settings.connect("changed::critical-color", Lang.bind ( { widget: critical_box }, setColor ) ); + critical_box.set_style ( 'background-color: ' + this.account_settings.get_string ( 'critical-color' ) ); + this.account_settings.connect("changed::critical-color", Lang.bind ( { widget: critical_box }, setColor ) ); this.criticalBoxes [ this.server ] = new St.Label({ text: String(_status['CRITICAL']) }) critical_box.add_child ( this.criticalBoxes [ this.server ] ); @@ -135,7 +144,7 @@ class Indicator extends PanelMenu.Button { _iconBin.child = _icon; this._buttonMenu.actor.add_actor(_iconBin); - this._mainLabel = new St.Label({ style_class: 'monito-title', text: 'Monito Checker', x_expand: true }); + this._mainLabel = new St.Label ( { style_class: 'monito-title', text: 'Monito Checker', x_expand: true } ); this._buttonMenu.actor.add_actor(this._mainLabel); this._prefsButton = this._createButton ( 'preferences-system-symbolic', 'Preferences', this._onPreferencesActivate ); @@ -184,57 +193,63 @@ class Indicator extends PanelMenu.Button { updateStatus ( ) { - for ( let _server of Preferences.getServersList() ) + if ( ! this.serverlogic.refresh ( this ) ) { - let _account_settings = Preferences.getAccountSettings ( this.server ); - - let type = _account_settings.get_string("type"); - let username = _account_settings.get_string("username"); - let password = _account_settings.get_string("password"); - let urlcgi = _account_settings.get_string("urlcgi"); - - if ( ! urlcgi ) - { - monitoLog ( 'Not updating monito because no URL configured' ); - } - else - { - let _serverLogic; - if ( type == 'Icinga' ) - _serverLogic = new Icinga ( this.server ); - else if ( type == 'Icinga2' ) - _serverLogic = new Icinga2 ( this.server ); - - if ( ! _serverLogic.refresh ( this ) ) - { - this.warningBoxes[this.server].set_text ( '…' ); - this.criticalBoxes[this.server].set_text ( '…' ); - // TODO: Add display of error if any - } - } + this.warningBoxes[this.server].set_text ( '…' ); + this.criticalBoxes[this.server].set_text ( '…' ); } this.setupTimeout ( ); } - createHeaderBin ( status, text, col ) { - let _widths = [ 300, 300, 200, 50, 600 ]; - let _bin = new St.Bin({ - style_class: 'monito-service-' + status, - width: col.width, - x_expand: col.expand, - child: new St.Button ( { + createHeaderBin ( colName ) { + let col = column_definitions [ colName ]; + + let _box = new St.BoxLayout ( { vertical: false, + x_expand: true } ); + _box.add_child ( new St.Label ( { text: col.label, + x_expand: true, + x_align: Clutter.ActorAlign.START } ) ); + + let _iconBin = new St.Bin ( { x_align: Clutter.ActorAlign.END }); + _box.add_child ( _iconBin ); + + let _iconName = ''; + let _sortOrder = Preferences.getSortOrder ( this.server ); + if ( _sortOrder.indexOf ( colName + '+' ) >= 0 ) + _iconName = 'view-sort-descending-symbolic'; + else if ( _sortOrder.indexOf ( colName + '-' ) >= 0 ) + _iconName = 'view-sort-ascending-symbolic'; + + this.sortIcons [ colName ] = new St.Icon ( { + style_class: 'monito-button-icon', + icon_name: _iconName, + icon_size: 16, + } ); + _iconBin.child = this.sortIcons [ colName ]; + + let _button = new St.Button ( { x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, width: col.width, reactive: true, can_focus: true, track_hover: true, - accessible_name: text, + accessible_name: col.label, style_class: 'button', - label: col.name, - }) + } ); + _button.column = colName; + + _button.add_actor ( _box ); + _button.connect ( 'clicked', Lang.bind ( this, this._onSortColumnClick ) ); + + let _bin = new St.Bin({ + style_class: 'monito-service', + width: col.width, + x_expand: col.expand, + child: _button, }); + return _bin; } @@ -249,14 +264,14 @@ class Indicator extends PanelMenu.Button { return _bin; } - refreshUI ( serverLogic ) { - this.initStatus ( ); // Specialize ! + refreshUI ( ) { + this.initStatus ( ); this._box.remove_all_children(); - if ( serverLogic.error ) + if ( this.serverlogic.error ) { - this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: serverLogic.error } ) ); + this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: this.serverlogic.error } ) ); return; } @@ -265,8 +280,10 @@ class Indicator extends PanelMenu.Button { x_expand: true }); this._box.add_child(headerBox); - for ( let col of column_definitions ) - headerBox.add_child ( this.createHeaderBin ( '', col.name, col ) ); + + let _columns = Preferences.getColumns ( this.server ); + for ( let _col of _columns ) + headerBox.add_child ( this.createHeaderBin ( _col ) ); let scrollBox = new St.ScrollView ( { hscrollbar_policy: St.PolicyType.NEVER, enable_mouse_scrolling: true, } ); @@ -279,29 +296,28 @@ class Indicator extends PanelMenu.Button { }); scrollBox.add_actor(tableBox); - for ( let i = 0 ; i < serverLogic.status.service_status.length ; i ++ ) + for ( let entry of this.serverlogic.getProcessedStatus ( ) ) { - _status [ serverLogic.status.service_status[i].status ] ++; - if ( serverLogic.status.service_status[i].status != 'OK' ) + _status [ entry.status ] ++; + if ( entry.status != 'OK' ) { let infoBox = new St.BoxLayout({ - style_class: 'monito-service-line monito-service-line-' + serverLogic.status.service_status[i].status, + style_class: 'monito-service-line monito-service-line-' + entry.status, track_hover: true, x_expand: true, }); tableBox.add_child(infoBox); - for ( let col of column_definitions ) + let _columns = Preferences.getColumns ( this.server ); + for ( let _col of _columns ) { - infoBox.add_child ( this.createBin ( serverLogic.status.service_status[i].status, - serverLogic.status.service_status[i][col.name], - col ) ); + infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); } } } - this.warningBoxes[serverLogic.server].set_text ( String(_status.WARNING) ); - this.criticalBoxes[serverLogic.server].set_text ( String(_status.CRITICAL) ); + this.warningBoxes[this.serverlogic.server].set_text ( String(_status.WARNING) ); + this.criticalBoxes[this.serverlogic.server].set_text ( String(_status.CRITICAL) ); return; } @@ -319,6 +335,26 @@ class Indicator extends PanelMenu.Button { return 0; } + _onSortColumnClick ( button ) { + monitoLog ( 'column >> ' + button.column ); + + let _sortOrder = Preferences.getSortOrder ( this.server ); + let _columns = Preferences.getColumns ( this.server ); + + let _indexPlus = _sortOrder.indexOf ( button.column + '+' ); + let _indexMinus = _sortOrder.indexOf ( button.column + '-' ); + + if ( _indexPlus >= 0 ) + _sortOrder [ _indexPlus ] = button.column + '-'; + else if ( _indexMinus >= 0 ) + _sortOrder.splice ( _indexMinus, 1 ); + else + _sortOrder.unshift ( button.column + '+' ); + + Preferences.setSortOrder ( this.server, _sortOrder ); + this.refreshUI ( ); + } + _createButton ( icon, text, callback ) { let button = new St.Button({ x_align: Clutter.ActorAlign.END, diff --git a/prefs.js b/prefs.js index 4c8fc44..6907999 100644 --- a/prefs.js +++ b/prefs.js @@ -174,6 +174,30 @@ function getAccountSettings ( id ) } +function getSortOrder ( server ) +{ + return this.getAccountSettings ( server ) . get_strv ( 'columns-order' ); +} + + +function setSortOrder ( server, sort_order ) +{ + return this.getAccountSettings ( server ) . set_strv ( 'columns-order', sort_order ); +} + + +function getColumns ( server ) +{ + return this.getAccountSettings ( server ) . get_strv ( 'columns' ); +} + + +function setColumns ( server, columns ) +{ + return this.getAccountSettings ( server ) . set_strv ( 'columns', columns ); +} + + function createAccountWidgets ( isActive ) { // Accounts diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 228d517..95bdcf0 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -93,11 +93,11 @@ - ['service_description'] + ['status','host_name','service_display_name','last_check','attempts','status_information'] - - [0] + + ['host_name+','service_display_name+'] diff --git a/servers/genericserver.js b/servers/genericserver.js new file mode 100644 index 0000000..e399943 --- /dev/null +++ b/servers/genericserver.js @@ -0,0 +1,76 @@ +/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Monito Gnome-Shell extension + Copyright (C) 2021 Benjamin Drieu + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +const { Soup } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Lang = imports.lang; +const Main = imports.ui.main; +const Me = ExtensionUtils.getCurrentExtension(); +const Preferences = Me.imports.prefs; + +let _httpSession; + + +class GenericServer { + constructor ( _server, _serverType = 'Generic' ) + { + log ( '>>> New %s server #%s'.format ( _serverType, _server ) ); + + this.server = _server; + } + + getProcessedStatus ( ) + { + let status = this.status.service_status; + + this.columns = Preferences.getColumns(this.server); + this.sortOrder = Preferences.getSortOrder(this.server); + + log ( this.sortOrder ); + status = status.sort ( Lang.bind ( this, this.compareServices ) ); + + return status; + } + + + compareServices ( a, b ) + { + for ( let _comparison of this.sortOrder ) + { + let _name = _comparison.substring ( 0, _comparison.length - 1 ); + let _order = _comparison.substring ( _comparison.length - 1, _comparison.length ); + if ( _name && _order && _name in a && _name in b ) + { + let _result = a [ _name ] . localeCompare ( b [ _name ] ); + if ( _result != 0 ) + { + if ( _order == '-' ) + return - _result; + else + return _result; + } + } + } + return 0; + } +} diff --git a/servers/icinga.js b/servers/icinga.js index bf46a6f..bb34d84 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -27,14 +27,14 @@ const Lang = imports.lang; const Main = imports.ui.main; const Me = ExtensionUtils.getCurrentExtension(); const Preferences = Me.imports.prefs; +const GenericServer = Me.imports.servers.genericserver.GenericServer; let _httpSession; -class Icinga { +class Icinga extends GenericServer { constructor ( _server ) { - log ( '>>> New Icinga #' + _server ); - this.server = _server + super(_server, 'Icinga'); } refresh ( extension ) { diff --git a/servers/icinga2.js b/servers/icinga2.js index b78ea26..12f7d47 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -27,15 +27,15 @@ const Lang = imports.lang; const Main = imports.ui.main; const Me = ExtensionUtils.getCurrentExtension(); const Preferences = Me.imports.prefs; +const GenericServer = Me.imports.servers.genericserver.GenericServer; let _httpSession; -class Icinga2 { +class Icinga2 extends GenericServer { constructor ( _server ) { - log ( '>>> New Icinga2 #' + _server ); - this.server = _server - } + super(_server, 'Icinga2'); + } refresh ( extension ) { this.extension = extension; From 775f7210095dc4dbf070c7b5c1c722d14c8688ed Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 19 Nov 2021 22:16:45 +0100 Subject: [PATCH 10/55] Add button + change defaults --- extension.js | 32 ++++++++++++++++++- ...ll.extensions.monito@drieu.org.gschema.xml | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/extension.js b/extension.js index 5ab724f..fbfde89 100644 --- a/extension.js +++ b/extension.js @@ -61,6 +61,7 @@ const column_definitions = { last_check: { label: _('Last check'), width: 200, expand: false, }, attempts: { label: _('Attempts'), width: 50, expand: false, }, status_information: { label: _('Information'), width: 600, expand: true, }, + actions: { label: 'Actions', width: 100, expand: true, special: 'actions' }, }; @@ -150,6 +151,9 @@ class Indicator extends PanelMenu.Button { this._prefsButton = this._createButton ( 'preferences-system-symbolic', 'Preferences', this._onPreferencesActivate ); this._buttonMenu.actor.add_child (this._prefsButton); + this._updateButton = this._createButton ( 'mail-send-receive-symbolic', 'Reload', this.updateStatus ); // Implement this + this._buttonMenu.actor.add_child (this._updateButton ); + this._reloadButton = this._createButton ( 'view-refresh-symbolic', 'Reload', this.updateStatus ); this._buttonMenu.actor.add_child (this._reloadButton ); @@ -254,12 +258,36 @@ class Indicator extends PanelMenu.Button { } createBin ( status, text, col ) { + let _child; + + if ( ! col [ 'special' ] ) + _child = new St.Label({ style_class: 'monito-label', text: text }); + else if ( col.special == 'actions' ) + { + _child = new St.BoxLayout ( { x_expand: true, + x_align: Clutter.ActorAlign.FILL, + vertical: false } ); + let _button = new St.Button ( { + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.CENTER, + can_focus: true, + track_hover: true, + } ); + _button.child = new St.Icon ( { + icon_name: 'view-refresh-symbolic', + icon_size: 16, + width: 16, + height: 16, + } ); + _child.add_child ( _button ); + } + let _bin = new St.Bin({ style_class: 'monito-service-' + status, track_hover: true, width: col.width, x_expand: col.expand, - child: new St.Label({ style_class: 'monito-label', text: text }) + child: _child, }); return _bin; } @@ -284,6 +312,7 @@ class Indicator extends PanelMenu.Button { let _columns = Preferences.getColumns ( this.server ); for ( let _col of _columns ) headerBox.add_child ( this.createHeaderBin ( _col ) ); + headerBox.add_child ( this.createHeaderBin ( 'actions' ) ); let scrollBox = new St.ScrollView ( { hscrollbar_policy: St.PolicyType.NEVER, enable_mouse_scrolling: true, } ); @@ -313,6 +342,7 @@ class Indicator extends PanelMenu.Button { { infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); } + infoBox.add_child ( this.createBin ( entry.status, '', column_definitions [ 'actions' ] ) ); } } diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 95bdcf0..13128c4 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -97,7 +97,7 @@ - ['host_name+','service_display_name+'] + ['status+', 'host_name+','service_display_name+'] From 886501cba2c63380c1f6907a488f5121c56e084d Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 19 Nov 2021 23:07:45 +0100 Subject: [PATCH 11/55] IHM tweaks --- extension.js | 11 ++++++----- stylesheet.css | 8 +++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/extension.js b/extension.js index fbfde89..9621c99 100644 --- a/extension.js +++ b/extension.js @@ -58,7 +58,7 @@ const column_definitions = { status: { label: _('Status'), width: 50, expand: false, }, host_name: { label: _('Host name'), width: 300, expand: false, }, service_display_name: { label: _('Service'), width: 300, expand: false, }, - last_check: { label: _('Last check'), width: 200, expand: false, }, + last_check: { label: _('Last check'), width: 200, expand: false, type: 'date' }, attempts: { label: _('Attempts'), width: 50, expand: false, }, status_information: { label: _('Information'), width: 600, expand: true, }, actions: { label: 'Actions', width: 100, expand: true, special: 'actions' }, @@ -151,7 +151,7 @@ class Indicator extends PanelMenu.Button { this._prefsButton = this._createButton ( 'preferences-system-symbolic', 'Preferences', this._onPreferencesActivate ); this._buttonMenu.actor.add_child (this._prefsButton); - this._updateButton = this._createButton ( 'mail-send-receive-symbolic', 'Reload', this.updateStatus ); // Implement this + this._updateButton = this._createButton ( 'emblem-synchronizing-symbolic', 'Reload', this.updateStatus ); // Implement this this._buttonMenu.actor.add_child (this._updateButton ); this._reloadButton = this._createButton ( 'view-refresh-symbolic', 'Reload', this.updateStatus ); @@ -261,17 +261,18 @@ class Indicator extends PanelMenu.Button { let _child; if ( ! col [ 'special' ] ) - _child = new St.Label({ style_class: 'monito-label', text: text }); + _child = new St.Label({ style_class: 'monito-label', text: text, }); else if ( col.special == 'actions' ) { _child = new St.BoxLayout ( { x_expand: true, x_align: Clutter.ActorAlign.FILL, vertical: false } ); let _button = new St.Button ( { + style_class: 'button small-button', + x_expand: true, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, can_focus: true, - track_hover: true, } ); _button.child = new St.Icon ( { icon_name: 'view-refresh-symbolic', @@ -393,7 +394,7 @@ class Indicator extends PanelMenu.Button { can_focus: true, track_hover: true, accessible_name: text, - style_class: 'button', + style_class: 'button big-button', }); button.child = new St.Icon({ diff --git a/stylesheet.css b/stylesheet.css index f84c52c..1c7804c 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -92,6 +92,12 @@ margin: 0px; } -.button { +.big-button { padding: 12px !important; } + +.small-button { + padding: 3px !important; +} + + From 76319c11d591df23377a2d853a7cdd84b7559d44 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 22 Nov 2021 17:36:55 +0100 Subject: [PATCH 12/55] Some refactoring --- extension.js | 16 ++++---- servers/genericserver.js | 77 ++++++++++++++++++++++++++++++++++++--- servers/icinga.js | 79 ++++++++++++++-------------------------- servers/icinga2.js | 77 +++++++++++---------------------------- 4 files changed, 129 insertions(+), 120 deletions(-) diff --git a/extension.js b/extension.js index 9621c99..ef6cba4 100644 --- a/extension.js +++ b/extension.js @@ -85,9 +85,9 @@ class Indicator extends PanelMenu.Button { let type = this.account_settings.get_string ( "type" ); if ( type == 'Icinga' ) - this.serverlogic = new Icinga ( this.server ); + this.serverLogic = new Icinga ( this.server ); else if ( type == 'Icinga2' ) - this.serverlogic = new Icinga2 ( this.server ); + this.serverLogic = new Icinga2 ( this.server ); this.initUI ( ); } @@ -197,7 +197,7 @@ class Indicator extends PanelMenu.Button { updateStatus ( ) { - if ( ! this.serverlogic.refresh ( this ) ) + if ( ! this.serverLogic.refresh ( this ) ) { this.warningBoxes[this.server].set_text ( '…' ); this.criticalBoxes[this.server].set_text ( '…' ); @@ -298,9 +298,9 @@ class Indicator extends PanelMenu.Button { this._box.remove_all_children(); - if ( this.serverlogic.error ) + if ( this.serverLogic.error ) { - this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: this.serverlogic.error } ) ); + this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: this.serverLogic.error } ) ); return; } @@ -326,7 +326,7 @@ class Indicator extends PanelMenu.Button { }); scrollBox.add_actor(tableBox); - for ( let entry of this.serverlogic.getProcessedStatus ( ) ) + for ( let entry of this.serverLogic.getProcessedStatus ( ) ) { _status [ entry.status ] ++; if ( entry.status != 'OK' ) @@ -347,8 +347,8 @@ class Indicator extends PanelMenu.Button { } } - this.warningBoxes[this.serverlogic.server].set_text ( String(_status.WARNING) ); - this.criticalBoxes[this.serverlogic.server].set_text ( String(_status.CRITICAL) ); + this.warningBoxes[this.server].set_text ( String(_status.WARNING) ); + this.criticalBoxes[this.server].set_text ( String(_status.CRITICAL) ); return; } diff --git a/servers/genericserver.js b/servers/genericserver.js index e399943..24b2dd1 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -28,23 +28,88 @@ const Main = imports.ui.main; const Me = ExtensionUtils.getCurrentExtension(); const Preferences = Me.imports.prefs; -let _httpSession; - class GenericServer { constructor ( _server, _serverType = 'Generic' ) { log ( '>>> New %s server #%s'.format ( _serverType, _server ) ); - this.server = _server; + this._server = _server; + this._settings = Preferences.getAccountSettings ( this._server ); + this._httpSession = null; + this._url = null; } + getServer ( ) + { + return this._server; + } + + buildURL ( ) + { + if ( ! this._settings ) + this._settings = Preferences.getAccountSettings ( this._server ); + + this.type = this._settings.get_string ( "type" ); + this.username = this._settings.get_string ( "username" ); + this.name = this._settings.get_string ( "name" ); + this.password = this._settings.get_string ( "password" ); + this.urlcgi = this._settings.get_string ( "urlcgi" ); + + log ( 'monito server >>> ' + this._server ); + log ( 'monito name >>> ' + this.name ); + log ( 'monito type >>> ' + this.type ); + log ( 'monito username >>> ' + this.username ); + log ( 'monito urlcgi >>> ' + this.urlcgi ); + } + + + prepareHttp ( ) + { + if ( this._httpSession == null ) { + this._httpSession = new Soup.Session(); + this._httpSession.user_agent = Me.metadata.uuid; + } + } + + + authenticateAndSend ( message ) + { + let auth = new Soup.AuthBasic() + auth.authenticate ( this.username, this.password ); + message.request_headers.append ( "Authorization", auth.get_authorization ( message ) ); + + this._httpSession.queue_message ( message, Lang.bind (this, this.handleMessage ) ); + } + + + handleMessage ( _httpSession, message ) + { + this.status = { }; + this.status.service_status = [ ]; + + if ( message.status_code != Soup.Status.OK ) + { + log ( '>>> error: ' + message.reason_phrase ); + Main.notifyError ( 'Monito: ' + this.name, + 'URL: ' + this.urlcgi + "\n" + + message.status_code + ': ' + message.reason_phrase ); + this.error = message.reason_phrase; + return null; + } + else + { + return message.response_body.data; + } + } + + getProcessedStatus ( ) { let status = this.status.service_status; - this.columns = Preferences.getColumns(this.server); - this.sortOrder = Preferences.getSortOrder(this.server); + this.columns = Preferences.getColumns(this._server); + this.sortOrder = Preferences.getSortOrder(this._server); log ( this.sortOrder ); status = status.sort ( Lang.bind ( this, this.compareServices ) ); @@ -52,7 +117,7 @@ class GenericServer { return status; } - + compareServices ( a, b ) { for ( let _comparison of this.sortOrder ) diff --git a/servers/icinga.js b/servers/icinga.js index bb34d84..6c3723d 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -38,57 +38,34 @@ class Icinga extends GenericServer { } refresh ( extension ) { - let _settings = Preferences.getAccountSettings ( this.server ); - log ( 'monito server >>> ' + this.server ); - let type = _settings.get_string ( "type" ); - let username = _settings.get_string ( "username" ); - let name = _settings.get_string ( "name" ); - let password = _settings.get_string ( "password" ); - let urlcgi = _settings.get_string ( "urlcgi" ); + this.extension = extension; - log ( 'monito name >>> ' + name ); - log ( 'monito type >>> ' + type ); - log ( 'monito username >>> ' + username ); - log ( 'monito urlcgi >>> ' + urlcgi ); - - urlcgi = urlcgi.replace ( /^(https?:\/\/)/, '$1' + username + ':' + password + '@' ); - - if (_httpSession === undefined) { - _httpSession = new Soup.Session(); - _httpSession.user_agent = Me.metadata.uuid; - } - - try { - - let message = Soup.form_request_new_from_hash ( 'GET', urlcgi, { 'jsonoutput': '' } ); - - _httpSession.queue_message(message, Lang.bind - (this, function(_httpSession, message) - { - if ( message.status_code != Soup.Status.OK ) - { - log ( '>>> error: ' + message.reason_phrase ); - Main.notify ( 'Monito: ' + name, message.reason_phrase ); - this.status = null; - this.error = message.reason_phrase; - } - else - { - let json = JSON.parse(message.response_body.data); - this.status = json.status; - this.error = null; - } - - extension.refreshUI ( this ); - } - ) - ); - return this.status !== null; - } - catch (e) { - Main.notify(_('Zbeu!')); - log(e); - return false; - } + this.buildURL ( ); + this.prepareHttp ( ); } + + + prepareHttp ( ) + { + super.prepareHttp ( ); + + let message = Soup.form_request_new_from_hash ( 'GET', this.urlcgi, { 'jsonoutput': '' } ); + message.request_headers.append ( 'Accept', 'application/json' ); + + this.authenticateAndSend ( message ); + } + + + handleMessage ( _httpSession, message ) + { + let _data = super.handleMessage ( _httpSession, message ); + let json = JSON.parse ( _data ); + + this.status = json.status; + this.error = null; + this.extension.refreshUI ( this ); + + return this.status != { }; + } + } diff --git a/servers/icinga2.js b/servers/icinga2.js index 12f7d47..7a70057 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -29,8 +29,6 @@ const Me = ExtensionUtils.getCurrentExtension(); const Preferences = Me.imports.prefs; const GenericServer = Me.imports.servers.genericserver.GenericServer; -let _httpSession; - class Icinga2 extends GenericServer { constructor ( _server ) { @@ -39,72 +37,41 @@ class Icinga2 extends GenericServer { refresh ( extension ) { this.extension = extension; - this.settings = Preferences.getAccountSettings ( this.server ); - log ( 'monito server >>> ' + this.server ); - this.type = this.settings.get_string ( "type" ); - this.username = this.settings.get_string ( "username" ); - this.name = this.settings.get_string ( "name" ); - this.password = this.settings.get_string ( "password" ); - this.urlcgi = this.settings.get_string ( "urlcgi" ); - - log ( 'monito name >>> ' + this.name ); - log ( 'monito type >>> ' + this.type ); - log ( 'monito username >>> ' + this.username ); - log ( 'monito urlcgi >>> ' + this.urlcgi ); + this.buildURL ( ); this.prepareHttp ( ); } prepareHttp ( ) { - if (_httpSession === undefined) { - _httpSession = new Soup.Session(); - _httpSession.user_agent = Me.metadata.uuid; - } - + super.prepareHttp ( ); + let message = Soup.form_request_new_from_hash ( 'GET', this.urlcgi, { 'format': 'json' } ); message.request_headers.append ( 'Accept', 'application/json' ); - - let auth = new Soup.AuthBasic() - auth.authenticate ( this.username, this.password ); - message.request_headers.append ( "Authorization", auth.get_authorization ( message ) ); - - _httpSession.queue_message ( message, Lang.bind (this, this.handleMessage ) ); + + this.authenticateAndSend ( message ); } handleMessage ( _httpSession, message ) { - this.status = { }; - this.status.service_status = [ ]; - - if ( message.status_code != Soup.Status.OK ) - { - log ( '>>> error: ' + message.reason_phrase ); - Main.notifyError ( 'Monito: ' + this.name, - 'URL: ' + this.urlcgi + "\n" + - message.status_code + ': ' + message.reason_phrase ); - this.error = message.reason_phrase; - } - else - { - let json = JSON.parse(message.response_body.data); - let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; - - for ( var entry of json ) - { - this.status.service_status.push ( { - status: _statuses [ entry.service_state ], - host_name: entry.host_name, - service_display_name: entry.service_display_name, - attempts: entry.service_attempt, - status_information: entry.service_output, - last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), - } ); - } - this.error = null; - } + let _data = super.handleMessage ( _httpSession, message ); + let json = JSON.parse ( _data ); + let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; - this.extension.refreshUI ( this ); + for ( var entry of json ) + { + this.status.service_status.push ( { + status: _statuses [ entry.service_state ], + host_name: entry.host_name, + service_display_name: entry.service_display_name, + attempts: entry.service_attempt, + status_information: entry.service_output, + last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), + } ); + } + this.error = null; + + this.extension.refreshUI ( this ); return this.status != { }; } } From d533f08e4452ca01d45f2079fdded3bbc6cc76dd Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 22 Nov 2021 22:12:49 +0100 Subject: [PATCH 13/55] Add more loging + error control --- extension.js | 10 +++++++++- servers/icinga.js | 48 +++++++++++++++++++++++++++++++++++++++------- servers/icinga2.js | 31 +++++++++++++++++------------- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/extension.js b/extension.js index ef6cba4..44e525b 100644 --- a/extension.js +++ b/extension.js @@ -274,6 +274,8 @@ class Indicator extends PanelMenu.Button { y_align: Clutter.ActorAlign.CENTER, can_focus: true, } ); + _button.service = text; + _button.connect ( 'clicked', Lang.bind ( this, this._onRecheckButtonClick ) ); _button.child = new St.Icon ( { icon_name: 'view-refresh-symbolic', icon_size: 16, @@ -343,7 +345,7 @@ class Indicator extends PanelMenu.Button { { infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); } - infoBox.add_child ( this.createBin ( entry.status, '', column_definitions [ 'actions' ] ) ); + infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); } } @@ -386,6 +388,12 @@ class Indicator extends PanelMenu.Button { this.refreshUI ( ); } + _onRecheckButtonClick ( e ) + { + monitoLog ( JSON.stringify ( e.service ) ); + this.serverLogic.recheck ( e.service ); + } + _createButton ( icon, text, callback ) { let button = new St.Button({ x_align: Clutter.ActorAlign.END, diff --git a/servers/icinga.js b/servers/icinga.js index 6c3723d..4f48cff 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -55,17 +55,51 @@ class Icinga extends GenericServer { this.authenticateAndSend ( message ); } + recheck ( entry ) + { + let cmdcgi = this.urlcgi.replace ( /status.cgi/, 'cmd.cgi' ); + let params = { cmd_typ: '7', + cmd_mod: '2', + hostservice: '%s^%s'.format ( entry.host_name, entry.service_description.replaceAll ( ' ', '+' ) ), + start_time: '2021-11-22%2018:13:48', + force_check: 'on', + btnSubmit: 'Commit', + }; + let message = Soup.form_request_new_from_hash ( 'POST', cmdcgi, params ); + log ( JSON.stringify ( params ) ); + +// message.request_headers.append ( 'Accept', 'application/json' ); + this.authenticateAndSend ( message ); + // cmd_typ: 7 + // cmd_mod: 2 + // start_time: '2021-11-22 17:21:12' + // hostservice: srv03^servicename (avec encodage + à la place des ' ') + // force_check: on + } handleMessage ( _httpSession, message ) { - let _data = super.handleMessage ( _httpSession, message ); - let json = JSON.parse ( _data ); + let _data; + try { + _data = super.handleMessage ( _httpSession, message ); + if ( this.error ) + log ( 'Parent error ' + this.error ); + else + { + let json = JSON.parse ( _data ); + this.status = json.status; + this.error = null; + } - this.status = json.status; - this.error = null; - this.extension.refreshUI ( this ); - - return this.status != { }; + this.extension.refreshUI ( this ); + + return this.status != { }; + } + catch ( e ) + { + log ( e ); + log ( _data ); + } } } diff --git a/servers/icinga2.js b/servers/icinga2.js index 7a70057..c523dc0 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -55,21 +55,26 @@ class Icinga2 extends GenericServer { handleMessage ( _httpSession, message ) { let _data = super.handleMessage ( _httpSession, message ); - let json = JSON.parse ( _data ); - let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; - - for ( var entry of json ) + if ( this.error ) + log ( 'Parent error ' + this.error ); + else { - this.status.service_status.push ( { - status: _statuses [ entry.service_state ], - host_name: entry.host_name, - service_display_name: entry.service_display_name, - attempts: entry.service_attempt, - status_information: entry.service_output, - last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), - } ); + let json = JSON.parse ( _data ); + let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; + + for ( var entry of json ) + { + this.status.service_status.push ( { + status: _statuses [ entry.service_state ], + host_name: entry.host_name, + service_display_name: entry.service_display_name, + attempts: entry.service_attempt, + status_information: entry.service_output, + last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), + } ); + } + this.error = null; } - this.error = null; this.extension.refreshUI ( this ); return this.status != { }; From 5f70bdea9f8caeb1f25a3bf91ddce6a3227577c8 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 24 Nov 2021 17:04:47 +0100 Subject: [PATCH 14/55] Update prefs --- README.md | 1 - extension.js | 15 +++- prefs.js | 80 ++++++++++++------- ...ll.extensions.monito@drieu.org.gschema.xml | 16 ++++ servers/genericserver.js | 1 + servers/icinga.js | 1 - servers/icinga2.js | 1 - 7 files changed, 77 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index dbf8609..4e73783 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ Things I plan to add at some point or another: * Regexes to modify the output of services (to prune unecessary stuff) * Filters to hide services (acked ones, or depending on regexes) * Support for other (free as in free speech) monitoring servers - * Better tabular output of the services result (sorting, pagination) * Choose which columns are shown in services result * Better icinga2 support (use API because things are missing in the json output ?) diff --git a/extension.js b/extension.js index 44e525b..bc840fa 100644 --- a/extension.js +++ b/extension.js @@ -111,16 +111,18 @@ class Indicator extends PanelMenu.Button { let warning_box = new St.BoxLayout({ style_class: 'monito-warning-box monito-box' }); - warning_box.set_style ( 'background-color: ' + this.account_settings.get_string ( 'warning-color' ) ); + warning_box.set_style ( 'background-color: %s; color: %s'.format ( this.account_settings.get_string ( 'warning-color' ), this.account_settings.get_string ( 'warning-fg' ) ) ); this.account_settings.connect("changed::warning-color", Lang.bind ( { widget: warning_box }, setColor ) ); + this.account_settings.connect("changed::warning-fg", Lang.bind ( { widget: warning_box }, setColor ) ); this.warningBoxes [ this.server ] = new St.Label({ text: String(_status['WARNING']) }) warning_box.add_child ( this.warningBoxes [ this.server ] ); serverBox.add_child(warning_box); let critical_box = new St.BoxLayout({ style_class: 'monito-critical-box monito-box' }); - critical_box.set_style ( 'background-color: ' + this.account_settings.get_string ( 'critical-color' ) ); + critical_box.set_style ( 'background-color: %s; color: %s'.format ( this.account_settings.get_string ( 'critical-color' ), this.account_settings.get_string ( 'critical-fg' ) ) ); this.account_settings.connect("changed::critical-color", Lang.bind ( { widget: critical_box }, setColor ) ); + this.account_settings.connect("changed::critical-fg", Lang.bind ( { widget: critical_box }, setColor ) ); this.criticalBoxes [ this.server ] = new St.Label({ text: String(_status['CRITICAL']) }) critical_box.add_child ( this.criticalBoxes [ this.server ] ); @@ -465,6 +467,11 @@ function monitoLog ( msg ) function setColor (stgs, key) { - monitoLog ( stgs.get_string(key) ); - this.widget.set_style ( 'background-color: ' + stgs.get_string(key) ); +// monitoLog ( '> %s color %s'.format ( key, stgs.get_string(key) ) ); +// monitoLog ( '> style %s'.format ( style ) ); + let style = this.widget.get_style ( ); + if ( key.match ( /-fg$/ ) ) + this.widget.set_style ( style + ';color: ' + stgs.get_string(key) ); + else + this.widget.set_style ( style + ';background-color: ' + stgs.get_string(key) ); } diff --git a/prefs.js b/prefs.js index 6907999..f212044 100644 --- a/prefs.js +++ b/prefs.js @@ -43,15 +43,23 @@ const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito.account"; const SETTINGS_SCHEMA_ACCOUNT_PATH = "/org/gnome/shell/extensions/monito/account"; -const prefs = [ { type: Gtk.Entry, label: _('Name'), key: 'name' }, - { type: Gtk.ComboBoxText, label: _('Type'), key: 'type' }, - { type: Gtk.Entry, label: _('Username'), key: 'username' }, - { type: Gtk.Entry, label: _('Password'), key: 'password' }, - { type: Gtk.Entry, label: _('URL CGI'), key: 'urlcgi' }, - { type: Gtk.ColorButton, label: _('OK color'), key: 'ok-color', align: Gtk.Align.START }, - { type: Gtk.ColorButton, label: _('Warning color'), key: 'warning-color', align: Gtk.Align.START }, - { type: Gtk.ColorButton, label: _('Critical color'), key: 'critical-color', align: Gtk.Align.START }, - { type: Gtk.ColorButton, label: _('Unknown color'), key: 'unknown-color', align: Gtk.Align.START }, +const prefs = [ { type: Gtk.Entry, category: 'Settings', label: _('Name'), key: 'name' }, + { type: Gtk.ComboBoxText, category: 'Settings', label: _('Type'), key: 'type' }, + { type: Gtk.Entry, category: 'Settings', label: _('Username'), key: 'username' }, + { type: Gtk.Entry, category: 'Settings', label: _('Password'), key: 'password' }, + { type: Gtk.Entry, category: 'Settings', label: _('URL CGI'), key: 'urlcgi' }, + { type: Gtk.ColorButton, category: 'Colors', label: _('OK background color'), key: 'ok-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Warning background color'), key: 'warning-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Critical background color'), key: 'critical-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown background color'), key: 'unknown-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('OK color'), key: 'ok-fg', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Warning color'), key: 'warning-fg', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Critical color'), key: 'critical-fg', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown color'), key: 'unknown-fg', align: Gtk.Align.START }, + { type: Gtk.Entry, category: 'Filter', label: _('Only display hosts matching'), key: 'host-grep', align: Gtk.Align.START }, + { type: Gtk.Entry, category: 'Filter', label: _('Only display services matching'), key: 'service-grip', align: Gtk.Align.START }, + { type: Gtk.Entry, category: 'Filter', label: _('Do not display hosts matching'), key: 'host-filter-out', align: Gtk.Align.START }, + { type: Gtk.Entry, category: 'Filter', label: _('Do not display services matching'), key: 'service-filter-out', align: Gtk.Align.START }, ]; @@ -200,8 +208,31 @@ function setColumns ( server, columns ) function createAccountWidgets ( isActive ) { - // Accounts - this._accountsWidget = new Gtk.Grid ( { + this.prefWidgets = { }; + + this._accountsWidget = new Gtk.Notebook( { } ); + this._accountsWidgetContainer.pack_start(this._accountsWidget, true, true, 0); + + // Settings + this.createPrefWidgets ( _accountsWidget, 'Settings', isActive ); + this.prefWidgets['name'].connect('changed', Lang.bind(this, function () { + let _row = this.accountsChooser.get_selected_row(); + _row.get_child().label = this.prefWidgets['name'].text; + } ) ); + + // Colors + this.createPrefWidgets ( _accountsWidget, 'Colors', isActive ); + + // Filters + this.createPrefWidgets ( _accountsWidget, 'Filters', isActive ); + + this._accountsWidget.show_all(); +} + + +function createPrefWidgets ( noteBook, type, isActive ) +{ + let grid = new Gtk.Grid ( { halign: Gtk.Align.FILL, margin: 18, column_spacing: 12, @@ -209,26 +240,20 @@ function createAccountWidgets ( isActive ) visible: true, column_homogeneous: false, }); - this._accountsWidgetContainer.pack_start(this._accountsWidget, true, true, 0); + noteBook.append_page ( grid, new Gtk.Label ( { label: _(type), } ) ); - this.prefWidgets = { }; - - this._accountsWidget.attach ( new Gtk.Label({ - label: '' + _('Accounts settings') + '', - visible: true, - halign: Gtk.Align.START, - use_markup: true, - }), 0, 0, 2, 1 ); - - let y = 1; + let y = 0; for ( var prefEntry of prefs ) { + if ( prefEntry.category != type ) + continue; + let _label = new Gtk.Label({ label: prefEntry.label + ':', visible: true, halign: Gtk.Align.START, }); - this._accountsWidget.attach ( _label, 0, y, 1, 1 ); + grid.attach ( _label, 0, y, 1, 1 ); this.prefWidgets[prefEntry.key] = new prefEntry.type ( { halign: ( prefEntry.align ? prefEntry.align : Gtk.Align.FILL ), @@ -250,7 +275,7 @@ function createAccountWidgets ( isActive ) } ); } - this._accountsWidget.attach(this.prefWidgets[prefEntry.key], 1, y, 1, 1); + grid.attach(this.prefWidgets[prefEntry.key], 1, y, 1, 1); if ( prefEntry.type != Gtk.ComboBoxText ) { @@ -263,15 +288,8 @@ function createAccountWidgets ( isActive ) } y++; } - - this.prefWidgets['name'].connect('changed', Lang.bind(this, function () { - let _row = this.accountsChooser.get_selected_row(); - _row.get_child().label = this.prefWidgets['name'].text; - - } ) ); } - function activateAccountRow ( ) { if ( this._accountsWidget ) diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 13128c4..55dd680 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -92,6 +92,22 @@ '#e496f5' + + '#ffffff' + + + + '#ffffff' + + + + '#ffffff' + + + + '#ffffff' + + ['status','host_name','service_display_name','last_check','attempts','status_information'] diff --git a/servers/genericserver.js b/servers/genericserver.js index 24b2dd1..3069a68 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -87,6 +87,7 @@ class GenericServer { { this.status = { }; this.status.service_status = [ ]; + this.error = null; if ( message.status_code != Soup.Status.OK ) { diff --git a/servers/icinga.js b/servers/icinga.js index 4f48cff..3440c2b 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -88,7 +88,6 @@ class Icinga extends GenericServer { { let json = JSON.parse ( _data ); this.status = json.status; - this.error = null; } this.extension.refreshUI ( this ); diff --git a/servers/icinga2.js b/servers/icinga2.js index c523dc0..8f1df1c 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -73,7 +73,6 @@ class Icinga2 extends GenericServer { last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), } ); } - this.error = null; } this.extension.refreshUI ( this ); From b9d8ef572724169716a81bc1476ce372a3f5d8b8 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 24 Nov 2021 20:58:07 +0100 Subject: [PATCH 15/55] Refactor command sending --- servers/genericserver.js | 4 +-- servers/icinga.js | 57 +++++++++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/servers/genericserver.js b/servers/genericserver.js index 3069a68..aa2b553 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -73,13 +73,13 @@ class GenericServer { } - authenticateAndSend ( message ) + authenticateAndSend ( message, callback = this.handleMessage ) { let auth = new Soup.AuthBasic() auth.authenticate ( this.username, this.password ); message.request_headers.append ( "Authorization", auth.get_authorization ( message ) ); - this._httpSession.queue_message ( message, Lang.bind (this, this.handleMessage ) ); + this._httpSession.queue_message ( message, Lang.bind (this, callback ) ); } diff --git a/servers/icinga.js b/servers/icinga.js index 3440c2b..14d3c23 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -57,24 +57,28 @@ class Icinga extends GenericServer { recheck ( entry ) { + // TODO: perhaps use a better idea, like if the urlcgi param + // is a directory, then appendinstead of changing... use a method for that? let cmdcgi = this.urlcgi.replace ( /status.cgi/, 'cmd.cgi' ); - let params = { cmd_typ: '7', - cmd_mod: '2', - hostservice: '%s^%s'.format ( entry.host_name, entry.service_description.replaceAll ( ' ', '+' ) ), - start_time: '2021-11-22%2018:13:48', - force_check: 'on', - btnSubmit: 'Commit', - }; - let message = Soup.form_request_new_from_hash ( 'POST', cmdcgi, params ); - log ( JSON.stringify ( params ) ); + let d = new Date ( Date.now() ); + var datestring = '%04d-%02d-%02d+%02d:%02d:%02d'.format ( d.getFullYear(), d.getMonth() + 1, d.getDate(), + d.getHours(), d.getMinutes(), d.getSeconds() ); -// message.request_headers.append ( 'Accept', 'application/json' ); - this.authenticateAndSend ( message ); - // cmd_typ: 7 - // cmd_mod: 2 - // start_time: '2021-11-22 17:21:12' - // hostservice: srv03^servicename (avec encodage + à la place des ' ') - // force_check: on + // We have to do this manually since the default encoding of + // Soup.form_request_new_from_hash is not understood by Icinga ... a shame! + let params = 'cmd_typ=7&cmd_mod=2&host=%s&service=%s&start_time=%s&force_check=on&com_data=Recheck+by+Monito&btnSubmit=Commit' . + format ( encodeURI ( entry.host_name ), + encodeURI ( entry.service_description ), + encodeURI ( datestring ) ); + + let message = Soup.form_request_new_from_hash ( 'POST', cmdcgi, { } ); + message.request_body.truncate(); + message.request_body.append ( params ); + message.request_body.flatten(); + + // TODO: change button to spinner + + this.authenticateAndSend ( message, this.handleCMDMessage ); } handleMessage ( _httpSession, message ) @@ -101,4 +105,25 @@ class Icinga extends GenericServer { } } + handleCMDMessage ( _httpSession, message ) + { + let _data; + try { + _data = super.handleMessage ( _httpSession, message ); + if ( this.error ) + log ( 'Parent error ' + this.error ); + else + { + // if error, grep for class='errorMessage' + // else grep for class='successBox' + log ( 'Cmd output ' + _data ); + } + } + catch ( e ) + { + log ( e ); + log ( _data ); + } + } + } From 747778571a5fbc7d4fdd3ba80c79525d509030f2 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Thu, 25 Nov 2021 14:45:42 +0100 Subject: [PATCH 16/55] Remove transient refresh bug --- extension.js | 11 ++++------- servers/icinga.js | 2 +- servers/icinga2.js | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/extension.js b/extension.js index bc840fa..93cb01e 100644 --- a/extension.js +++ b/extension.js @@ -199,12 +199,7 @@ class Indicator extends PanelMenu.Button { updateStatus ( ) { - if ( ! this.serverLogic.refresh ( this ) ) - { - this.warningBoxes[this.server].set_text ( '…' ); - this.criticalBoxes[this.server].set_text ( '…' ); - } - + this.serverLogic.refresh ( this ); this.setupTimeout ( ); } @@ -302,9 +297,11 @@ class Indicator extends PanelMenu.Button { this._box.remove_all_children(); - if ( this.serverLogic.error ) + if ( this.serverLogic.error || ! this.serverLogic.status ) { this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: this.serverLogic.error } ) ); + this.warningBoxes[this.server].set_text ( '…' ); + this.criticalBoxes[this.server].set_text ( '…' ); return; } diff --git a/servers/icinga.js b/servers/icinga.js index 14d3c23..ebe361c 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -96,7 +96,7 @@ class Icinga extends GenericServer { this.extension.refreshUI ( this ); - return this.status != { }; + return ! this.error; } catch ( e ) { diff --git a/servers/icinga2.js b/servers/icinga2.js index 8f1df1c..04480b9 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -76,7 +76,7 @@ class Icinga2 extends GenericServer { } this.extension.refreshUI ( this ); - return this.status != { }; + return ! this.error; } } From 83c27bf84af97af9f1f781d24003c21dbfc7c875 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 29 Nov 2021 16:07:29 +0100 Subject: [PATCH 17/55] Refactoring --- extension.js | 62 +++++++++++++++---- prefs.js | 54 +++++++++------- ...ll.extensions.monito@drieu.org.gschema.xml | 41 ++++++++++++ servers/genericserver.js | 1 + 4 files changed, 122 insertions(+), 36 deletions(-) diff --git a/extension.js b/extension.js index 93cb01e..4d5be5a 100644 --- a/extension.js +++ b/extension.js @@ -75,9 +75,7 @@ class Indicator extends PanelMenu.Button { this.initStatus ( ); this.namesBoxes = { }; - this.warningBoxes = { }; - this.criticalBoxes = { }; - this.unknownBoxes = { }; + this.boxes = { }; this.sortIcons = { }; account_settings [ server ] = Preferences.getAccountSettings ( this.server ); @@ -108,15 +106,34 @@ class Indicator extends PanelMenu.Button { this.account_settings.bind ( 'name', this.namesBoxes [ this.server ], 'text', Gio.SettingsBindFlags.GET ); this.namesBoxes [ this.server ].connect ( 'button-press-event', Lang.bind ( this, function ( ) { this.setMenu ( this.menu ); } ) ); + + for ( var boxName of [ 'ok', 'warning', 'critical', 'unknown' ] ) + { + let _box = new St.BoxLayout( { style_class: 'monito-%s-box monito-box'.format(boxName), visible: false } ); + _box.set_style ( 'background-color: %s; color: %s'.format ( this.account_settings.get_string ( boxName + '-color' ), + this.account_settings.get_string ( boxName + '-fg' ) ) ); + this.account_settings.connect("changed::%s-color".format(boxName), Lang.bind ( { widget: _box }, setColor ) ); + this.account_settings.connect("changed::%s-fg".format(boxName), Lang.bind ( { widget: _box }, setColor ) ); + + this.boxes [ boxName ] = new St.Label ( { text: '' } ); + _box.add_child ( this.boxes [ boxName ] ); + serverBox.add_child(_box); + } + +/* + this.okBox = new St.Label({ text: String(_status['OK']), visible: false }) + ok_box.add_child ( this.okBox ); + serverBox.add_child(ok_box); + let warning_box = new St.BoxLayout({ style_class: 'monito-warning-box monito-box' }); warning_box.set_style ( 'background-color: %s; color: %s'.format ( this.account_settings.get_string ( 'warning-color' ), this.account_settings.get_string ( 'warning-fg' ) ) ); this.account_settings.connect("changed::warning-color", Lang.bind ( { widget: warning_box }, setColor ) ); this.account_settings.connect("changed::warning-fg", Lang.bind ( { widget: warning_box }, setColor ) ); - this.warningBoxes [ this.server ] = new St.Label({ text: String(_status['WARNING']) }) - warning_box.add_child ( this.warningBoxes [ this.server ] ); + this.warningBox = new St.Label({ text: String(_status['WARNING']), visible: false }) + warning_box.add_child ( this.warningBox ); serverBox.add_child(warning_box); let critical_box = new St.BoxLayout({ style_class: 'monito-critical-box monito-box' }); @@ -124,9 +141,10 @@ class Indicator extends PanelMenu.Button { this.account_settings.connect("changed::critical-color", Lang.bind ( { widget: critical_box }, setColor ) ); this.account_settings.connect("changed::critical-fg", Lang.bind ( { widget: critical_box }, setColor ) ); - this.criticalBoxes [ this.server ] = new St.Label({ text: String(_status['CRITICAL']) }) - critical_box.add_child ( this.criticalBoxes [ this.server ] ); + this.criticalBox = new St.Label({ text: String(_status['CRITICAL']), visible: false }) + critical_box.add_child ( this.criticalBox ); serverBox.add_child(critical_box); +*/ box.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM)); @@ -300,8 +318,9 @@ class Indicator extends PanelMenu.Button { if ( this.serverLogic.error || ! this.serverLogic.status ) { this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: this.serverLogic.error } ) ); - this.warningBoxes[this.server].set_text ( '…' ); - this.criticalBoxes[this.server].set_text ( '…' ); + this.boxes['ok'].set_text ( '…' ); + this.boxes['warning'].set_text ( '…' ); + this.boxes['critical'].set_text ( '…' ); return; } @@ -347,9 +366,28 @@ class Indicator extends PanelMenu.Button { infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); } } - - this.warningBoxes[this.server].set_text ( String(_status.WARNING) ); - this.criticalBoxes[this.server].set_text ( String(_status.CRITICAL) ); + + if ( _status.WARNING == 0 && _status.CRITICAL == 0 ) + { + this.boxes['ok'].set_text ( String(_status.OK) ); + this.boxes['ok'].get_parent().show ( ); + this.boxes['warning'].get_parent().hide ( ); + this.boxes['critical'].get_parent().hide ( ); + } + else + { + this.boxes['warning'].set_text ( String(_status.WARNING) ); + this.boxes['critical'].set_text ( String(_status.CRITICAL) ); + this.boxes['ok'].get_parent().hide ( ); + this.boxes['warning'].get_parent().show ( ); + this.boxes['critical'].get_parent().show ( ); + } + + if ( _status.UNKNOWN != 0 ) + { + this.boxes['unknown'].set_text ( String(_status.UNKNOWN) ); + this.boxes['unkown'].get_parent().show ( ); + } return; } diff --git a/prefs.js b/prefs.js index f212044..678a816 100644 --- a/prefs.js +++ b/prefs.js @@ -43,24 +43,31 @@ const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito.account"; const SETTINGS_SCHEMA_ACCOUNT_PATH = "/org/gnome/shell/extensions/monito/account"; -const prefs = [ { type: Gtk.Entry, category: 'Settings', label: _('Name'), key: 'name' }, - { type: Gtk.ComboBoxText, category: 'Settings', label: _('Type'), key: 'type' }, - { type: Gtk.Entry, category: 'Settings', label: _('Username'), key: 'username' }, - { type: Gtk.Entry, category: 'Settings', label: _('Password'), key: 'password' }, - { type: Gtk.Entry, category: 'Settings', label: _('URL CGI'), key: 'urlcgi' }, - { type: Gtk.ColorButton, category: 'Colors', label: _('OK background color'), key: 'ok-color', align: Gtk.Align.START }, - { type: Gtk.ColorButton, category: 'Colors', label: _('Warning background color'), key: 'warning-color', align: Gtk.Align.START }, - { type: Gtk.ColorButton, category: 'Colors', label: _('Critical background color'), key: 'critical-color', align: Gtk.Align.START }, - { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown background color'), key: 'unknown-color', align: Gtk.Align.START }, - { type: Gtk.ColorButton, category: 'Colors', label: _('OK color'), key: 'ok-fg', align: Gtk.Align.START }, - { type: Gtk.ColorButton, category: 'Colors', label: _('Warning color'), key: 'warning-fg', align: Gtk.Align.START }, - { type: Gtk.ColorButton, category: 'Colors', label: _('Critical color'), key: 'critical-fg', align: Gtk.Align.START }, - { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown color'), key: 'unknown-fg', align: Gtk.Align.START }, - { type: Gtk.Entry, category: 'Filter', label: _('Only display hosts matching'), key: 'host-grep', align: Gtk.Align.START }, - { type: Gtk.Entry, category: 'Filter', label: _('Only display services matching'), key: 'service-grip', align: Gtk.Align.START }, - { type: Gtk.Entry, category: 'Filter', label: _('Do not display hosts matching'), key: 'host-filter-out', align: Gtk.Align.START }, - { type: Gtk.Entry, category: 'Filter', label: _('Do not display services matching'), key: 'service-filter-out', align: Gtk.Align.START }, - ]; +const prefs = [ + { type: Gtk.Entry, category: 'Settings', label: _('Name'), key: 'name' }, + { type: Gtk.ComboBoxText, category: 'Settings', label: _('Type'), key: 'type' }, + { type: Gtk.Entry, category: 'Settings', label: _('Username'), key: 'username' }, + { type: Gtk.Entry, category: 'Settings', label: _('Password'), key: 'password' }, + { type: Gtk.Entry, category: 'Settings', label: _('URL CGI'), key: 'urlcgi' }, + { type: Gtk.ColorButton, category: 'Colors', label: _('OK background color'), key: 'ok-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Warning background color'), key: 'warning-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Critical background color'), key: 'critical-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown background color'), key: 'unknown-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('OK color'), key: 'ok-fg', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Warning color'), key: 'warning-fg', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Critical color'), key: 'critical-fg', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown color'), key: 'unknown-fg', align: Gtk.Align.START }, + { type: Gtk.Entry, category: 'Filters', label: _('Only display hosts matching'), key: 'host-grep' }, + { type: Gtk.Entry, category: 'Filters', label: _('Only display services matching'), key: 'service-grep' }, + { type: Gtk.Entry, category: 'Filters', label: _('Do not display hosts matching'), key: 'host-filter-out' }, + { type: Gtk.Entry, category: 'Filters', label: _('Do not display services matching'), key: 'service-filter-out' }, + { type: Gtk.Entry, category: 'Replacements', label: _('Host match regexp ...'), key: 'host-match' }, + { type: Gtk.Entry, category: 'Replacements', label: _('... to replace with'), key: 'host-replace' }, + { type: Gtk.Entry, category: 'Replacements', label: _('Service match regexp ...'), key: 'service-match' }, + { type: Gtk.Entry, category: 'Replacements', label: _('... to replace with'), key: 'service-replace' }, + { type: Gtk.Entry, category: 'Replacements', label: _('Status info match regexp ...'), key: 'status-info-match' }, + { type: Gtk.Entry, category: 'Replacements', label: _('... to replace with'), key: 'status-info-replace' }, +]; // Like 'extension.js' this is used for any one-time setup like translations. @@ -213,18 +220,17 @@ function createAccountWidgets ( isActive ) this._accountsWidget = new Gtk.Notebook( { } ); this._accountsWidgetContainer.pack_start(this._accountsWidget, true, true, 0); + for ( var _tab of [ 'Settings', 'Colors', 'Filters', 'Replacements' ] ) + { + this.createPrefWidgets ( _accountsWidget, _tab, isActive ); + } + // Settings - this.createPrefWidgets ( _accountsWidget, 'Settings', isActive ); this.prefWidgets['name'].connect('changed', Lang.bind(this, function () { let _row = this.accountsChooser.get_selected_row(); _row.get_child().label = this.prefWidgets['name'].text; } ) ); - // Colors - this.createPrefWidgets ( _accountsWidget, 'Colors', isActive ); - - // Filters - this.createPrefWidgets ( _accountsWidget, 'Filters', isActive ); this._accountsWidget.show_all(); } diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 55dd680..dff84b2 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -116,5 +116,46 @@ ['status+', 'host_name+','service_display_name+'] + + '' + + + + '' + + + + '' + + + + '' + + + + '' + + + + '' + + + + '' + + + + '' + + + + '' + + + + '' + + + diff --git a/servers/genericserver.js b/servers/genericserver.js index aa2b553..f94fe1a 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -92,6 +92,7 @@ class GenericServer { if ( message.status_code != Soup.Status.OK ) { log ( '>>> error: ' + message.reason_phrase ); + log ( '>>> data: ' + message.data ); Main.notifyError ( 'Monito: ' + this.name, 'URL: ' + this.urlcgi + "\n" + message.status_code + ': ' + message.reason_phrase ); From b207dd2fc76286ae2a30b3f0f5e222a6092a9ffc Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 29 Nov 2021 18:15:18 +0100 Subject: [PATCH 18/55] Display OK lines in case no errors (so as to allow recheck and friends --- extension.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extension.js b/extension.js index 4d5be5a..e16b869 100644 --- a/extension.js +++ b/extension.js @@ -346,10 +346,13 @@ class Indicator extends PanelMenu.Button { }); scrollBox.add_actor(tableBox); + for ( let entryCount of this.serverLogic.getProcessedStatus ( ) ) + _status [ entryCount.status ] ++; + for ( let entry of this.serverLogic.getProcessedStatus ( ) ) { - _status [ entry.status ] ++; - if ( entry.status != 'OK' ) + if ( ( ! _status [ 'WARNING' ] && ! _status [ 'CRITICAL' ] && ! _status [ 'UNKNOWN' ] && entry.status == 'OK' ) || + ( ( _status [ 'WARNING' ] || _status [ 'CRITICAL' ] || _status [ 'UNKNOWN' ] ) && entry.status != 'OK' ) ) { let infoBox = new St.BoxLayout({ style_class: 'monito-service-line monito-service-line-' + entry.status, From 13cabc55d0fb6b9542d0b97831f00db540b6e509 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 29 Nov 2021 18:15:50 +0100 Subject: [PATCH 19/55] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e73783..f6f172e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Things I plan to add at some point or another: * Buttons to operate on services à la nagstamon (recheck, connect via SSH, etc.) * Regexes to modify the output of services (to prune unecessary stuff) * Filters to hide services (acked ones, or depending on regexes) - * Support for other (free as in free speech) monitoring servers + * Support for other (as in free speech) monitoring servers * Choose which columns are shown in services result * Better icinga2 support (use API because things are missing in the json output ?) From bd99ca0850d364f825a8efd522deeb96f0d07891 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 29 Nov 2021 18:19:58 +0100 Subject: [PATCH 20/55] Add dep on Makefile --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 10e5ea5..fdd6711 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,9 @@ reload: schema busctl --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s 'Meta.restart("Restarting…")' -schema: +schema: schemas/gschemas.compiled + +schemas/gschemas.compiled: schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml glib-compile-schemas --strict schemas/ pref: From 64806075b66b0f3f942ed805c36b4de18ab898bc Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 29 Nov 2021 23:04:17 +0100 Subject: [PATCH 21/55] Improve color display of services --- extension.js | 79 +++++++++++++++++++++++++++++++++------------------- prefs.js | 11 ++++---- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/extension.js b/extension.js index e16b869..ec3c294 100644 --- a/extension.js +++ b/extension.js @@ -111,8 +111,7 @@ class Indicator extends PanelMenu.Button { for ( var boxName of [ 'ok', 'warning', 'critical', 'unknown' ] ) { let _box = new St.BoxLayout( { style_class: 'monito-%s-box monito-box'.format(boxName), visible: false } ); - _box.set_style ( 'background-color: %s; color: %s'.format ( this.account_settings.get_string ( boxName + '-color' ), - this.account_settings.get_string ( boxName + '-fg' ) ) ); + _box.set_style ( this.getStyleForRow ( boxName ) ); this.account_settings.connect("changed::%s-color".format(boxName), Lang.bind ( { widget: _box }, setColor ) ); this.account_settings.connect("changed::%s-fg".format(boxName), Lang.bind ( { widget: _box }, setColor ) ); @@ -121,31 +120,6 @@ class Indicator extends PanelMenu.Button { serverBox.add_child(_box); } -/* - this.okBox = new St.Label({ text: String(_status['OK']), visible: false }) - ok_box.add_child ( this.okBox ); - serverBox.add_child(ok_box); - - - let warning_box = new St.BoxLayout({ style_class: 'monito-warning-box monito-box' }); - warning_box.set_style ( 'background-color: %s; color: %s'.format ( this.account_settings.get_string ( 'warning-color' ), this.account_settings.get_string ( 'warning-fg' ) ) ); - this.account_settings.connect("changed::warning-color", Lang.bind ( { widget: warning_box }, setColor ) ); - this.account_settings.connect("changed::warning-fg", Lang.bind ( { widget: warning_box }, setColor ) ); - - this.warningBox = new St.Label({ text: String(_status['WARNING']), visible: false }) - warning_box.add_child ( this.warningBox ); - serverBox.add_child(warning_box); - - let critical_box = new St.BoxLayout({ style_class: 'monito-critical-box monito-box' }); - critical_box.set_style ( 'background-color: %s; color: %s'.format ( this.account_settings.get_string ( 'critical-color' ), this.account_settings.get_string ( 'critical-fg' ) ) ); - this.account_settings.connect("changed::critical-color", Lang.bind ( { widget: critical_box }, setColor ) ); - this.account_settings.connect("changed::critical-fg", Lang.bind ( { widget: critical_box }, setColor ) ); - - this.criticalBox = new St.Label({ text: String(_status['CRITICAL']), visible: false }) - critical_box.add_child ( this.criticalBox ); - serverBox.add_child(critical_box); -*/ - box.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM)); this.menu_new = new PopupMenu.PopupMenu(this, Clutter.ActorAlign.START, St.Side.TOP, 0); @@ -301,7 +275,7 @@ class Indicator extends PanelMenu.Button { } let _bin = new St.Bin({ - style_class: 'monito-service-' + status, +// style: 'background: purple', track_hover: true, width: col.width, x_expand: col.expand, @@ -310,6 +284,36 @@ class Indicator extends PanelMenu.Button { return _bin; } + getStyleForRow ( boxName, row = 0 ) + { + let bgColor = this.account_settings.get_string ( boxName + '-color' ); + let fgColor = this.account_settings.get_string ( boxName + '-fg' ); + + if ( row % 2 ) + { + bgColor = this.lightenColor(bgColor, 16); + fgColor = this.lightenColor(fgColor, 16); + } + else + { + bgColor = this.lightenColor(bgColor, -16); + fgColor = this.lightenColor(fgColor, -16); + } + + monitoLog ( 'background-color: %s; color: %s' . format ( bgColor, fgColor ) ); + + return 'background-color: %s; color: %s' . format ( bgColor, fgColor ); + } + + lightenColor ( col, amt ) { + if ( col.substring(0,1) == '#' ) + col = col.substring ( 1 ); + col = parseInt(col, 16); + return '#%06x'.format (Math.max(Math.min((col & 0x0000FF) + amt,0x0000FF),0) | + (Math.max(Math.min((((col >> 8) & 0x00FF) + amt),0x0000FF),0) << 8) | + (Math.max(Math.min(((col >> 16) + amt),0x0000FF),0) << 16)); + } + refreshUI ( ) { this.initStatus ( ); @@ -349,13 +353,18 @@ class Indicator extends PanelMenu.Button { for ( let entryCount of this.serverLogic.getProcessedStatus ( ) ) _status [ entryCount.status ] ++; + let _row = 0; for ( let entry of this.serverLogic.getProcessedStatus ( ) ) { if ( ( ! _status [ 'WARNING' ] && ! _status [ 'CRITICAL' ] && ! _status [ 'UNKNOWN' ] && entry.status == 'OK' ) || ( ( _status [ 'WARNING' ] || _status [ 'CRITICAL' ] || _status [ 'UNKNOWN' ] ) && entry.status != 'OK' ) ) { + monitoLog ( this.account_settings.get_string ( entry.status.toLowerCase() + '-color' ) ); + + let _style = this.getStyleForRow ( entry.status.toLowerCase(), _row ); let infoBox = new St.BoxLayout({ - style_class: 'monito-service-line monito-service-line-' + entry.status, + style_class: 'monito-service-line', + style: _style, track_hover: true, x_expand: true, }); @@ -364,9 +373,21 @@ class Indicator extends PanelMenu.Button { let _columns = Preferences.getColumns ( this.server ); for ( let _col of _columns ) { + if ( _col == 'host_name' && this.account_settings.get_string ( 'host-match' ) ) + entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'host-match' ), 'i' ), + this.account_settings.get_string ( 'host-replace' ) ); + else if ( _col == 'service_display_name' && this.account_settings.get_string ( 'service-match' ) ) + entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'service-match' ), 'i' ), + this.account_settings.get_string ( 'service-replace' ) ); + else if ( _col == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) + entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), + this.account_settings.get_string ( 'status-info-replace' ) ); + infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); } infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); + + _row ++; } } diff --git a/prefs.js b/prefs.js index 678a816..28c5fc9 100644 --- a/prefs.js +++ b/prefs.js @@ -59,13 +59,13 @@ const prefs = [ { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown color'), key: 'unknown-fg', align: Gtk.Align.START }, { type: Gtk.Entry, category: 'Filters', label: _('Only display hosts matching'), key: 'host-grep' }, { type: Gtk.Entry, category: 'Filters', label: _('Only display services matching'), key: 'service-grep' }, - { type: Gtk.Entry, category: 'Filters', label: _('Do not display hosts matching'), key: 'host-filter-out' }, - { type: Gtk.Entry, category: 'Filters', label: _('Do not display services matching'), key: 'service-filter-out' }, - { type: Gtk.Entry, category: 'Replacements', label: _('Host match regexp ...'), key: 'host-match' }, + { type: Gtk.Entry, category: 'Filters', label: _('Do not display hosts matching'), key: 'host-filter-out' }, + { type: Gtk.Entry, category: 'Filters', label: _('Do not display services matching'), key: 'service-filter-out' }, + { type: Gtk.Entry, category: 'Replacements', label: _('Host regexp ...'), key: 'host-match' }, { type: Gtk.Entry, category: 'Replacements', label: _('... to replace with'), key: 'host-replace' }, - { type: Gtk.Entry, category: 'Replacements', label: _('Service match regexp ...'), key: 'service-match' }, + { type: Gtk.Entry, category: 'Replacements', label: _('Service regexp ...'), key: 'service-match' }, { type: Gtk.Entry, category: 'Replacements', label: _('... to replace with'), key: 'service-replace' }, - { type: Gtk.Entry, category: 'Replacements', label: _('Status info match regexp ...'), key: 'status-info-match' }, + { type: Gtk.Entry, category: 'Replacements', label: _('Status info regexp ...'), key: 'status-info-match' }, { type: Gtk.Entry, category: 'Replacements', label: _('... to replace with'), key: 'status-info-replace' }, ]; @@ -258,6 +258,7 @@ function createPrefWidgets ( noteBook, type, isActive ) label: prefEntry.label + ':', visible: true, halign: Gtk.Align.START, + use_markup: true, }); grid.attach ( _label, 0, y, 1, 1 ); From 93d4e485ae09013c239f40cf447b1cb4c57a393a Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 15:27:25 +0100 Subject: [PATCH 22/55] Some cleanup + add Icinga2 API server --- extension.js | 188 +++++++++++++++++++++++++++------------------------ 1 file changed, 98 insertions(+), 90 deletions(-) diff --git a/extension.js b/extension.js index ec3c294..5226ad5 100644 --- a/extension.js +++ b/extension.js @@ -49,6 +49,7 @@ const Convenience = Me.imports.convenience; const GenericServer = Me.imports.servers.genericserver.GenericServer; const Icinga = Me.imports.servers.icinga.Icinga; const Icinga2 = Me.imports.servers.icinga2.Icinga2; +const Icinga2API = Me.imports.servers.icinga2api.Icinga2API; const Preferences = Me.imports.prefs; let settings = Convenience.getSettings(SETTINGS_SCHEMA); @@ -86,6 +87,8 @@ class Indicator extends PanelMenu.Button { this.serverLogic = new Icinga ( this.server ); else if ( type == 'Icinga2' ) this.serverLogic = new Icinga2 ( this.server ); + else if ( type == 'Icinga2API' ) + this.serverLogic = new Icinga2API ( this.server ); this.initUI ( ); } @@ -249,8 +252,11 @@ class Indicator extends PanelMenu.Button { createBin ( status, text, col ) { let _child; + if ( ! text ) + text = '…'; + if ( ! col [ 'special' ] ) - _child = new St.Label({ style_class: 'monito-label', text: text, }); + _child = new St.Label({ style_class: 'monito-label', text: text.toString(), }); else if ( col.special == 'actions' ) { _child = new St.BoxLayout ( { x_expand: true, @@ -291,17 +297,15 @@ class Indicator extends PanelMenu.Button { if ( row % 2 ) { - bgColor = this.lightenColor(bgColor, 16); - fgColor = this.lightenColor(fgColor, 16); + bgColor = this.lightenColor(bgColor, 12); + fgColor = this.lightenColor(fgColor, 12); } else { - bgColor = this.lightenColor(bgColor, -16); - fgColor = this.lightenColor(fgColor, -16); + bgColor = this.lightenColor(bgColor, -12); + fgColor = this.lightenColor(fgColor, -12); } - monitoLog ( 'background-color: %s; color: %s' . format ( bgColor, fgColor ) ); - return 'background-color: %s; color: %s' . format ( bgColor, fgColor ); } @@ -315,102 +319,108 @@ class Indicator extends PanelMenu.Button { } refreshUI ( ) { - this.initStatus ( ); + try { + this.initStatus ( ); - this._box.remove_all_children(); + this._box.remove_all_children(); - if ( this.serverLogic.error || ! this.serverLogic.status ) - { - this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: this.serverLogic.error } ) ); - this.boxes['ok'].set_text ( '…' ); - this.boxes['warning'].set_text ( '…' ); - this.boxes['critical'].set_text ( '…' ); - return; - } - - let headerBox = new St.BoxLayout({ - hover: true, - x_expand: true - }); - this._box.add_child(headerBox); + if ( this.serverLogic.error || ! this.serverLogic.status ) + { + this._box.add_child ( new St.Label ( { style_class: 'monito-network-error', text: this.serverLogic.error } ) ); + this.boxes['ok'].set_text ( '…' ); + this.boxes['warning'].set_text ( '…' ); + this.boxes['critical'].set_text ( '…' ); + return; + } + + let headerBox = new St.BoxLayout({ + hover: true, + x_expand: true + }); + this._box.add_child(headerBox); - let _columns = Preferences.getColumns ( this.server ); - for ( let _col of _columns ) - headerBox.add_child ( this.createHeaderBin ( _col ) ); - headerBox.add_child ( this.createHeaderBin ( 'actions' ) ); + let _columns = Preferences.getColumns ( this.server ); + for ( let _col of _columns ) + headerBox.add_child ( this.createHeaderBin ( _col ) ); + headerBox.add_child ( this.createHeaderBin ( 'actions' ) ); - let scrollBox = new St.ScrollView ( { hscrollbar_policy: St.PolicyType.NEVER, - enable_mouse_scrolling: true, } ); - this._box.add_child(scrollBox); + let scrollBox = new St.ScrollView ( { hscrollbar_policy: St.PolicyType.NEVER, + enable_mouse_scrolling: true, } ); + this._box.add_child(scrollBox); - let tableBox = new St.BoxLayout({ - hover: true, - vertical: true, - x_expand: true, - }); - scrollBox.add_actor(tableBox); + let tableBox = new St.BoxLayout({ + hover: true, + vertical: true, + x_expand: true, + }); + scrollBox.add_actor(tableBox); - for ( let entryCount of this.serverLogic.getProcessedStatus ( ) ) - _status [ entryCount.status ] ++; + let processedStatus = this.serverLogic.getProcessedStatus ( ) + for ( let entryCount of processedStatus ) + _status [ entryCount.status ] ++; - let _row = 0; - for ( let entry of this.serverLogic.getProcessedStatus ( ) ) - { - if ( ( ! _status [ 'WARNING' ] && ! _status [ 'CRITICAL' ] && ! _status [ 'UNKNOWN' ] && entry.status == 'OK' ) || - ( ( _status [ 'WARNING' ] || _status [ 'CRITICAL' ] || _status [ 'UNKNOWN' ] ) && entry.status != 'OK' ) ) + let _row = 0; + for ( let entry of processedStatus ) { - monitoLog ( this.account_settings.get_string ( entry.status.toLowerCase() + '-color' ) ); + if ( ( ! _status [ 'WARNING' ] && ! _status [ 'CRITICAL' ] && ! _status [ 'UNKNOWN' ] && entry.status == 'OK' ) || + ( ( _status [ 'WARNING' ] || _status [ 'CRITICAL' ] || _status [ 'UNKNOWN' ] ) && entry.status != 'OK' ) ) + { + let _style = this.getStyleForRow ( entry.status.toLowerCase(), _row ); + let infoBox = new St.BoxLayout({ + style_class: 'monito-service-line', + style: _style, + track_hover: true, + x_expand: true, + }); + tableBox.add_child(infoBox); - let _style = this.getStyleForRow ( entry.status.toLowerCase(), _row ); - let infoBox = new St.BoxLayout({ - style_class: 'monito-service-line', - style: _style, - track_hover: true, - x_expand: true, - }); - tableBox.add_child(infoBox); + let _columns = Preferences.getColumns ( this.server ); + for ( let _col of _columns ) + { + if ( _col == 'host_name' && this.account_settings.get_string ( 'host-match' ) ) + entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'host-match' ), 'i' ), + this.account_settings.get_string ( 'host-replace' ) ); + else if ( _col == 'service_display_name' && this.account_settings.get_string ( 'service-match' ) ) + entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'service-match' ), 'i' ), + this.account_settings.get_string ( 'service-replace' ) ); + else if ( _col == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) + entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), + this.account_settings.get_string ( 'status-info-replace' ) ); - let _columns = Preferences.getColumns ( this.server ); - for ( let _col of _columns ) - { - if ( _col == 'host_name' && this.account_settings.get_string ( 'host-match' ) ) - entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'host-match' ), 'i' ), - this.account_settings.get_string ( 'host-replace' ) ); - else if ( _col == 'service_display_name' && this.account_settings.get_string ( 'service-match' ) ) - entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'service-match' ), 'i' ), - this.account_settings.get_string ( 'service-replace' ) ); - else if ( _col == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) - entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), - this.account_settings.get_string ( 'status-info-replace' ) ); + infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); + } + infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); - infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); - } - infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); - - _row ++; + _row ++; + } } - } - if ( _status.WARNING == 0 && _status.CRITICAL == 0 ) - { - this.boxes['ok'].set_text ( String(_status.OK) ); - this.boxes['ok'].get_parent().show ( ); - this.boxes['warning'].get_parent().hide ( ); - this.boxes['critical'].get_parent().hide ( ); - } - else - { - this.boxes['warning'].set_text ( String(_status.WARNING) ); - this.boxes['critical'].set_text ( String(_status.CRITICAL) ); - this.boxes['ok'].get_parent().hide ( ); - this.boxes['warning'].get_parent().show ( ); - this.boxes['critical'].get_parent().show ( ); - } + if ( _status.WARNING == 0 && _status.CRITICAL == 0 ) + { + this.boxes['ok'].set_text ( String(_status.OK) ); + this.boxes['ok'].get_parent().show ( ); + this.boxes['warning'].get_parent().hide ( ); + this.boxes['critical'].get_parent().hide ( ); + } + else + { + this.boxes['warning'].set_text ( String(_status.WARNING) ); + this.boxes['critical'].set_text ( String(_status.CRITICAL) ); + this.boxes['ok'].get_parent().hide ( ); + this.boxes['warning'].get_parent().show ( ); + this.boxes['critical'].get_parent().show ( ); + } - if ( _status.UNKNOWN != 0 ) + if ( _status.UNKNOWN != 0 ) + { + this.boxes['unknown'].set_text ( String(_status.UNKNOWN) ); + this.boxes['unkown'].get_parent().show ( ); + } + } + catch ( e ) { - this.boxes['unknown'].set_text ( String(_status.UNKNOWN) ); - this.boxes['unkown'].get_parent().show ( ); + monitoLog ( 'RefreshUI error: ' + e ); + monitoLog ( e.stack ); } return; @@ -430,8 +440,6 @@ class Indicator extends PanelMenu.Button { } _onSortColumnClick ( button ) { - monitoLog ( 'column >> ' + button.column ); - let _sortOrder = Preferences.getSortOrder ( this.server ); let _columns = Preferences.getColumns ( this.server ); From ffebd4d6a0df7a159ba59cdfe9980a3e29de5531 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 15:27:37 +0100 Subject: [PATCH 23/55] Add Icinga2 API server --- prefs.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/prefs.js b/prefs.js index 28c5fc9..483d3db 100644 --- a/prefs.js +++ b/prefs.js @@ -277,8 +277,10 @@ function createPrefWidgets ( noteBook, type, isActive ) } else if ( prefEntry.key == 'type' ) { - [ 'Icinga', 'Icinga2' ].forEach((item) => { - this.prefWidgets[prefEntry.key].append_text(item); + [ { name: 'Icinga', value: 'Icinga server' }, + { name: 'Icinga2', value: 'Icinga2 server (without API)' }, + { name: 'Icinga2API', value: 'Icinga2 server (with API)' } ].forEach((item) => { + this.prefWidgets[prefEntry.key].insert ( -1, item.name, item.value ); } ); } From c4a9402e4e01e58b42b41e3d1cb87ad11137976b Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 15:28:05 +0100 Subject: [PATCH 24/55] Quiet the logs + add some bulletproof checks --- servers/genericserver.js | 18 ++++---- servers/icinga2api.js | 90 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 servers/icinga2api.js diff --git a/servers/genericserver.js b/servers/genericserver.js index f94fe1a..06adc50 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -56,11 +56,11 @@ class GenericServer { this.password = this._settings.get_string ( "password" ); this.urlcgi = this._settings.get_string ( "urlcgi" ); - log ( 'monito server >>> ' + this._server ); - log ( 'monito name >>> ' + this.name ); - log ( 'monito type >>> ' + this.type ); - log ( 'monito username >>> ' + this.username ); - log ( 'monito urlcgi >>> ' + this.urlcgi ); +// log ( 'monito server >>> ' + this._server ); +// log ( 'monito name >>> ' + this.name ); +// log ( 'monito type >>> ' + this.type ); +// log ( 'monito username >>> ' + this.username ); +// log ( 'monito urlcgi >>> ' + this.urlcgi ); } @@ -91,8 +91,8 @@ class GenericServer { if ( message.status_code != Soup.Status.OK ) { - log ( '>>> error: ' + message.reason_phrase ); - log ( '>>> data: ' + message.data ); + log ( '>>> Error: ' + message.reason_phrase ); + log ( '>>> Data: ' + message.data ); Main.notifyError ( 'Monito: ' + this.name, 'URL: ' + this.urlcgi + "\n" + message.status_code + ': ' + message.reason_phrase ); @@ -113,7 +113,6 @@ class GenericServer { this.columns = Preferences.getColumns(this._server); this.sortOrder = Preferences.getSortOrder(this._server); - log ( this.sortOrder ); status = status.sort ( Lang.bind ( this, this.compareServices ) ); return status; @@ -128,6 +127,9 @@ class GenericServer { let _order = _comparison.substring ( _comparison.length - 1, _comparison.length ); if ( _name && _order && _name in a && _name in b ) { + if ( ! a [ _name ] ) + return 1; + let _result = a [ _name ] . localeCompare ( b [ _name ] ); if ( _result != 0 ) { diff --git a/servers/icinga2api.js b/servers/icinga2api.js new file mode 100644 index 0000000..8047350 --- /dev/null +++ b/servers/icinga2api.js @@ -0,0 +1,90 @@ +/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Monito Gnome-Shell extension + Copyright (C) 2021 Benjamin Drieu + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +const { Soup } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Lang = imports.lang; +const Main = imports.ui.main; +const Me = ExtensionUtils.getCurrentExtension(); +const Preferences = Me.imports.prefs; +const GenericServer = Me.imports.servers.genericserver.GenericServer; + + +class Icinga2API extends GenericServer { + constructor ( _server ) { + super(_server, 'Icinga2 API'); + } + + refresh ( extension ) { + this.extension = extension; + + this.buildURL ( ); + this.prepareHttp ( ); + } + + prepareHttp ( ) + { + super.prepareHttp ( ); + + let message = Soup.form_request_new_from_hash ( 'GET', this.urlcgi, { 'format': 'json' } ); + message.request_headers.append ( 'Accept', 'application/json' ); + + this.authenticateAndSend ( message ); + } + + handleMessage ( _httpSession, message ) + { + let _data = super.handleMessage ( _httpSession, message ); + try + { + if ( this.error ) + log ( 'Parent error ' + this.error ); + else + { + let json = JSON.parse ( _data ); + let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; + + for ( var entry of json.results ) + { + this.status.service_status.push ( { + status: _statuses [ entry.attrs.state ], + host_name: entry.attrs.host_name, + service_display_name: entry.attrs.display_name, + attempts: '%d/%d'.format(entry.attrs.check_attempt,entry.attrs.max_check_attempts), + status_information: entry.attrs.last_check_result.output, + last_check: new Date ( entry.attrs.last_state_change * 1000 ) . toString(), + } ); + } + } + } + catch ( e ) + { + log ( '> ERROR: ' + e ); + log ( '> DATA: ' + _data ); + } + + this.extension.refreshUI ( this ); + return ! this.error; + } +} + From 07dc49fd13970e6b08075fd3d8ad59d82268f050 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 15:28:19 +0100 Subject: [PATCH 25/55] Add Icinga2 API server --- schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index dff84b2..66b7e4d 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -23,6 +23,7 @@ + From 42d4537b232ab6f1b2dc0a9cd1d614f3aa890df5 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 16:18:30 +0100 Subject: [PATCH 26/55] Implement recheck --- servers/icinga2api.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/servers/icinga2api.js b/servers/icinga2api.js index 8047350..e0f886f 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -31,6 +31,7 @@ const GenericServer = Me.imports.servers.genericserver.GenericServer; class Icinga2API extends GenericServer { + constructor ( _server ) { super(_server, 'Icinga2 API'); } @@ -46,13 +47,31 @@ class Icinga2API extends GenericServer { { super.prepareHttp ( ); - let message = Soup.form_request_new_from_hash ( 'GET', this.urlcgi, { 'format': 'json' } ); + let message = Soup.form_request_new_from_hash ( 'GET', this.urlcgi + '/objects/services', { } ); message.request_headers.append ( 'Accept', 'application/json' ); - this.authenticateAndSend ( message ); + this.authenticateAndSend ( message, this.handlePollMessage ); } - handleMessage ( _httpSession, message ) + recheck ( entry ) + { + let message = Soup.form_request_new_from_hash ( 'POST', this.urlcgi + '/actions/reschedule-check', { } ); + + let params = '{ "type": "Service", "filter": "host.name==\\"%s\\" && service.name==\\"%s\\"", "force": true, "pretty": true }' . format ( encodeURI ( entry.host_name ), encodeURI ( entry.service_display_name ) ); + + message.request_body.truncate(); + message.request_body.append ( params ); + message.request_body.flatten(); + + log ( '> Body: ' + message.request_body.data ); + + message.request_headers.append ( 'Accept', 'application/json' ); + log ( message.request_headers ); + + this.authenticateAndSend ( message, this.handleCMDMessage ); + } + + handlePollMessage ( _httpSession, message ) { let _data = super.handleMessage ( _httpSession, message ); try From c4245fe199473e2393060855b7c1d512872e8311 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 16:18:38 +0100 Subject: [PATCH 27/55] Better multi-server implementation --- extension.js | 5 +++-- servers/genericserver.js | 26 +++++++++++++++++++++++++- servers/icinga.js | 21 --------------------- servers/icinga2.js | 2 ++ 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/extension.js b/extension.js index 5226ad5..5ad7342 100644 --- a/extension.js +++ b/extension.js @@ -257,7 +257,7 @@ class Indicator extends PanelMenu.Button { if ( ! col [ 'special' ] ) _child = new St.Label({ style_class: 'monito-label', text: text.toString(), }); - else if ( col.special == 'actions' ) + else if ( col.special == 'actions' && this.serverLogic.canRecheck ) { _child = new St.BoxLayout ( { x_expand: true, x_align: Clutter.ActorAlign.FILL, @@ -389,7 +389,8 @@ class Indicator extends PanelMenu.Button { infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); } - infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); + if ( this.serverLogic.canRecheck ) + infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); _row ++; } diff --git a/servers/genericserver.js b/servers/genericserver.js index 06adc50..88f2b5c 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -29,7 +29,8 @@ const Me = ExtensionUtils.getCurrentExtension(); const Preferences = Me.imports.prefs; -class GenericServer { +class GenericServer { + constructor ( _server, _serverType = 'Generic' ) { log ( '>>> New %s server #%s'.format ( _serverType, _server ) ); @@ -38,6 +39,8 @@ class GenericServer { this._settings = Preferences.getAccountSettings ( this._server ); this._httpSession = null; this._url = null; + + this.canRecheck = true; } getServer ( ) @@ -105,6 +108,27 @@ class GenericServer { } } + handleCMDMessage ( _httpSession, message ) + { + let _data; + try { + _data = this.handleMessage ( _httpSession, message ); + if ( this.error ) + log ( 'Parent error ' + this.error ); + else + { + // if error, grep for class='errorMessage' + // else grep for class='successBox' + log ( 'Cmd output ' + _data ); + } + } + catch ( e ) + { + log ( e ); + log ( _data ); + } + } + getProcessedStatus ( ) { diff --git a/servers/icinga.js b/servers/icinga.js index ebe361c..d04909e 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -104,26 +104,5 @@ class Icinga extends GenericServer { log ( _data ); } } - - handleCMDMessage ( _httpSession, message ) - { - let _data; - try { - _data = super.handleMessage ( _httpSession, message ); - if ( this.error ) - log ( 'Parent error ' + this.error ); - else - { - // if error, grep for class='errorMessage' - // else grep for class='successBox' - log ( 'Cmd output ' + _data ); - } - } - catch ( e ) - { - log ( e ); - log ( _data ); - } - } } diff --git a/servers/icinga2.js b/servers/icinga2.js index 04480b9..05d2930 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -33,6 +33,8 @@ const GenericServer = Me.imports.servers.genericserver.GenericServer; class Icinga2 extends GenericServer { constructor ( _server ) { super(_server, 'Icinga2'); + + this.canRecheck = false; } refresh ( extension ) { From 5628b8efe536b0ad50466608062d2e62f21422c7 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 16:22:16 +0100 Subject: [PATCH 28/55] Wording --- prefs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prefs.js b/prefs.js index 483d3db..7d769b7 100644 --- a/prefs.js +++ b/prefs.js @@ -278,8 +278,8 @@ function createPrefWidgets ( noteBook, type, isActive ) else if ( prefEntry.key == 'type' ) { [ { name: 'Icinga', value: 'Icinga server' }, - { name: 'Icinga2', value: 'Icinga2 server (without API)' }, - { name: 'Icinga2API', value: 'Icinga2 server (with API)' } ].forEach((item) => { + { name: 'Icinga2', value: 'Icinga2 server (using Icingaweb2)' }, + { name: 'Icinga2API', value: 'Icinga2 server (using API)' } ].forEach((item) => { this.prefWidgets[prefEntry.key].insert ( -1, item.name, item.value ); } ); } From e194321288e6f3108b84c20500f0fc7090d44ce2 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 16:24:07 +0100 Subject: [PATCH 29/55] Update roadmap --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index f6f172e..e1d52dc 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,9 @@ inside of the gnome-shell panel (this could not be developed). Things I plan to add at some point or another: * Buttons to operate on services à la nagstamon (recheck, connect via SSH, etc.) - * Regexes to modify the output of services (to prune unecessary stuff) - * Filters to hide services (acked ones, or depending on regexes) + * Filters to hide services (acked, handled, ...) * Support for other (as in free speech) monitoring servers * Choose which columns are shown in services result - * Better icinga2 support (use API because things are missing in the json output ?) Things I will be unlikely to add unless a patch is provided: From 68e394508c14510ee8f5ace7dc0b8010896cfa6d Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 1 Dec 2021 16:24:18 +0100 Subject: [PATCH 30/55] Remove annoying notification --- servers/genericserver.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/servers/genericserver.js b/servers/genericserver.js index 88f2b5c..273c862 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -96,9 +96,10 @@ class GenericServer { { log ( '>>> Error: ' + message.reason_phrase ); log ( '>>> Data: ' + message.data ); - Main.notifyError ( 'Monito: ' + this.name, - 'URL: ' + this.urlcgi + "\n" + - message.status_code + ': ' + message.reason_phrase ); + // TODO: add pref for that +// Main.notifyError ( 'Monito: ' + this.name, +// 'URL: ' + this.urlcgi + "\n" + +// message.status_code + ': ' + message.reason_phrase ); this.error = message.reason_phrase; return null; } From 4b5881ffae9861eda74468c585c18cd6de70ce9e Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Thu, 2 Dec 2021 00:12:33 +0100 Subject: [PATCH 31/55] Tyop --- extension.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extension.js b/extension.js index 5ad7342..fd24644 100644 --- a/extension.js +++ b/extension.js @@ -259,8 +259,7 @@ class Indicator extends PanelMenu.Button { _child = new St.Label({ style_class: 'monito-label', text: text.toString(), }); else if ( col.special == 'actions' && this.serverLogic.canRecheck ) { - _child = new St.BoxLayout ( { x_expand: true, - x_align: Clutter.ActorAlign.FILL, + _child = new St.BoxLayout ( { x_expand: false, vertical: false } ); let _button = new St.Button ( { style_class: 'button small-button', @@ -330,6 +329,7 @@ class Indicator extends PanelMenu.Button { this.boxes['ok'].set_text ( '…' ); this.boxes['warning'].set_text ( '…' ); this.boxes['critical'].set_text ( '…' ); + this.boxes['unknown'].set_text ( '…' ); return; } @@ -402,6 +402,7 @@ class Indicator extends PanelMenu.Button { this.boxes['ok'].get_parent().show ( ); this.boxes['warning'].get_parent().hide ( ); this.boxes['critical'].get_parent().hide ( ); + this.boxes['unknown'].get_parent().hide ( ); } else { @@ -415,7 +416,7 @@ class Indicator extends PanelMenu.Button { if ( _status.UNKNOWN != 0 ) { this.boxes['unknown'].set_text ( String(_status.UNKNOWN) ); - this.boxes['unkown'].get_parent().show ( ); + this.boxes['unknown'].get_parent().show ( ); } } catch ( e ) From 5a4ebbf486efd12ad739248dcce0d81127e1054f Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Thu, 2 Dec 2021 00:28:51 +0100 Subject: [PATCH 32/55] Vertical align, at last :-) --- extension.js | 3 ++- stylesheet.css | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extension.js b/extension.js index fd24644..386375d 100644 --- a/extension.js +++ b/extension.js @@ -103,7 +103,8 @@ class Indicator extends PanelMenu.Button { box.add_child(serverBox); let name_box = new St.BoxLayout( { style_class: 'monito-namebox' } ); - this.namesBoxes [ this.server ] = new St.Label ( { text: this.account_settings.get_string ( 'name' ) } ); + this.namesBoxes [ this.server ] = new St.Label ( { text: this.account_settings.get_string ( 'name' ), + y_align: Clutter.ActorAlign.CENTER, } ); name_box.add_child ( this.namesBoxes [ this.server ] ); serverBox.add_child(name_box); this.account_settings.bind ( 'name', this.namesBoxes [ this.server ], 'text', Gio.SettingsBindFlags.GET ); diff --git a/stylesheet.css b/stylesheet.css index 1c7804c..fd7d475 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -44,7 +44,6 @@ .monito-namebox { margin-right: .5em; - vertical-align: middle; } .monito-critical-box, .monito-service-CRITICAL, .monito-service-line-CRITICAL { From d7408fdf0affeb88a9d7aaaacfc1dbb639537d5f Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Thu, 2 Dec 2021 17:52:08 +0100 Subject: [PATCH 33/55] Fix replacement bugs + implement correctly text-based filters --- extension.js | 7 ++- prefs.js | 2 + ...ll.extensions.monito@drieu.org.gschema.xml | 8 ++++ servers/genericserver.js | 47 ++++++++++++++++++- servers/icinga.js | 4 +- servers/icinga2api.js | 2 +- 6 files changed, 64 insertions(+), 6 deletions(-) diff --git a/extension.js b/extension.js index 386375d..69779c5 100644 --- a/extension.js +++ b/extension.js @@ -378,6 +378,7 @@ class Indicator extends PanelMenu.Button { let _columns = Preferences.getColumns ( this.server ); for ( let _col of _columns ) { + entry [ 'real_' + _col ] = entry [ _col ]; if ( _col == 'host_name' && this.account_settings.get_string ( 'host-match' ) ) entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'host-match' ), 'i' ), this.account_settings.get_string ( 'host-replace' ) ); @@ -387,7 +388,7 @@ class Indicator extends PanelMenu.Button { else if ( _col == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), this.account_settings.get_string ( 'status-info-replace' ) ); - + infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); } if ( this.serverLogic.canRecheck ) @@ -419,6 +420,10 @@ class Indicator extends PanelMenu.Button { this.boxes['unknown'].set_text ( String(_status.UNKNOWN) ); this.boxes['unknown'].get_parent().show ( ); } + else + { + this.boxes['unknown'].get_parent().hide ( ); + } } catch ( e ) { diff --git a/prefs.js b/prefs.js index 7d769b7..a873679 100644 --- a/prefs.js +++ b/prefs.js @@ -59,8 +59,10 @@ const prefs = [ { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown color'), key: 'unknown-fg', align: Gtk.Align.START }, { type: Gtk.Entry, category: 'Filters', label: _('Only display hosts matching'), key: 'host-grep' }, { type: Gtk.Entry, category: 'Filters', label: _('Only display services matching'), key: 'service-grep' }, + { type: Gtk.Entry, category: 'Filters', label: _('Only display status info matching'), key: 'status-info-grep' }, { type: Gtk.Entry, category: 'Filters', label: _('Do not display hosts matching'), key: 'host-filter-out' }, { type: Gtk.Entry, category: 'Filters', label: _('Do not display services matching'), key: 'service-filter-out' }, + { type: Gtk.Entry, category: 'Filters', label: _('Do not display status info matching'), key: 'status-info-filter-out' }, { type: Gtk.Entry, category: 'Replacements', label: _('Host regexp ...'), key: 'host-match' }, { type: Gtk.Entry, category: 'Replacements', label: _('... to replace with'), key: 'host-replace' }, { type: Gtk.Entry, category: 'Replacements', label: _('Service regexp ...'), key: 'service-match' }, diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 66b7e4d..361727e 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -125,6 +125,10 @@ '' + + '' + + '' @@ -133,6 +137,10 @@ '' + + '' + + '' diff --git a/servers/genericserver.js b/servers/genericserver.js index 273c862..416bae6 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -135,14 +135,57 @@ class GenericServer { { let status = this.status.service_status; - this.columns = Preferences.getColumns(this._server); - this.sortOrder = Preferences.getSortOrder(this._server); + this.columns = Preferences.getColumns ( this._server ); + this.sortOrder = Preferences.getSortOrder ( this._server ); + status = this.filterStatus ( status ); status = status.sort ( Lang.bind ( this, this.compareServices ) ); return status; } + + filterStatus ( status ) + { + let filters = [ { prefKey: 'service-grep', + entryKey: 'service_display_name', + positive: true }, + { prefKey: 'service-filter-out', + entryKey: 'service_display_name', + positive: false }, + { prefKey: 'host-grep', + entryKey: 'host_name', + positive: true }, + { prefKey: 'host-filter-out', + entryKey: 'host_name', + positive: false }, + { prefKey: 'status-info-grep', + entryKey: 'status_information', + positive: true }, + { prefKey: 'status-info-filter-out', + entryKey: 'status_information', + positive: false } ]; + + for ( var _filter of filters ) + _filter.value = this._settings.get_string ( _filter.prefKey ); + + entries: for ( var i = 0 ; i < status.length ; i ++ ) + { + for ( var _filter of filters ) + { + if ( _filter['value'] && + ( status[i][_filter['entryKey']].match ( new RegExp ( _filter['value'], 'i' ) ) <= 0 ) == _filter['positive'] ) + { +// log ( '> NOT MATCHING: %s =~ %s == %s ' . format ( status[i][_filter['entryKey']], _filter['value'], _filter['positive'] ) ); + status.splice ( i, 1 ); + i --; // This has been removed, so get back one step. + continue entries; + } + } + } + + return status; + } compareServices ( a, b ) { diff --git a/servers/icinga.js b/servers/icinga.js index d04909e..d64034a 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -67,8 +67,8 @@ class Icinga extends GenericServer { // We have to do this manually since the default encoding of // Soup.form_request_new_from_hash is not understood by Icinga ... a shame! let params = 'cmd_typ=7&cmd_mod=2&host=%s&service=%s&start_time=%s&force_check=on&com_data=Recheck+by+Monito&btnSubmit=Commit' . - format ( encodeURI ( entry.host_name ), - encodeURI ( entry.service_description ), + format ( encodeURI ( entry.real_host_name ), + encodeURI ( entry.real_service_display_name ), encodeURI ( datestring ) ); let message = Soup.form_request_new_from_hash ( 'POST', cmdcgi, { } ); diff --git a/servers/icinga2api.js b/servers/icinga2api.js index e0f886f..b7ea4c1 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -57,7 +57,7 @@ class Icinga2API extends GenericServer { { let message = Soup.form_request_new_from_hash ( 'POST', this.urlcgi + '/actions/reschedule-check', { } ); - let params = '{ "type": "Service", "filter": "host.name==\\"%s\\" && service.name==\\"%s\\"", "force": true, "pretty": true }' . format ( encodeURI ( entry.host_name ), encodeURI ( entry.service_display_name ) ); + let params = '{ "type": "Service", "filter": "host.name==\\"%s\\" && service.name==\\"%s\\"", "force": true, "pretty": true }' . format ( encodeURI ( entry.real_host_name ), encodeURI ( entry.real_service_display_name ) ); message.request_body.truncate(); message.request_body.append ( params ); From 8e952badffb5ecaa5c2fb43c45af0904ecfc5e4b Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Thu, 2 Dec 2021 19:05:44 +0100 Subject: [PATCH 34/55] Implement acked monitoring + filter out --- README.md | 3 ++- extension.js | 15 +++++++++++++-- prefs.js | 10 ++++++++++ ....shell.extensions.monito@drieu.org.gschema.xml | 6 +++++- servers/genericserver.js | 12 ++++++++++-- servers/icinga2.js | 1 + servers/icinga2api.js | 1 + 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e1d52dc..b1f0ccf 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ inside of the gnome-shell panel (this could not be developed). Things I plan to add at some point or another: * Buttons to operate on services à la nagstamon (recheck, connect via SSH, etc.) - * Filters to hide services (acked, handled, ...) + * Filters to hide services (handled, scheduled downtime, host down, ...) * Support for other (as in free speech) monitoring servers * Choose which columns are shown in services result @@ -38,3 +38,4 @@ Things I will be unlikely to add unless a patch is provided: * Support for broken HTTPS certs * Notification for new alerts + * Display list of down hosts (I am not interested in host monitoring but services) diff --git a/extension.js b/extension.js index 69779c5..2fcdf6a 100644 --- a/extension.js +++ b/extension.js @@ -59,6 +59,7 @@ const column_definitions = { status: { label: _('Status'), width: 50, expand: false, }, host_name: { label: _('Host name'), width: 300, expand: false, }, service_display_name: { label: _('Service'), width: 300, expand: false, }, + has_been_acknowledged: { label: _('Ack'), width: 50, expand: false, align: Clutter.ActorAlign.CENTER, style: 'font-weight:bold;' }, last_check: { label: _('Last check'), width: 200, expand: false, type: 'date' }, attempts: { label: _('Attempts'), width: 50, expand: false, }, status_information: { label: _('Information'), width: 600, expand: true, }, @@ -253,11 +254,14 @@ class Indicator extends PanelMenu.Button { createBin ( status, text, col ) { let _child; - if ( ! text ) + if ( text === undefined ) text = '…'; if ( ! col [ 'special' ] ) - _child = new St.Label({ style_class: 'monito-label', text: text.toString(), }); + _child = new St.Label ( { style_class: 'monito-label', + text: text.toString(), + x_align: ( col.align ? col.align : Clutter.ActorAlign.START ), + style: ( col.style ? col.style : '' ) } ); else if ( col.special == 'actions' && this.serverLogic.canRecheck ) { _child = new St.BoxLayout ( { x_expand: false, @@ -388,6 +392,13 @@ class Indicator extends PanelMenu.Button { else if ( _col == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), this.account_settings.get_string ( 'status-info-replace' ) ); + else if ( _col == 'has_been_acknowledged' ) + { + if ( entry [ _col ] ) + entry [ _col ] = '✔'; // … or ✅🗹 ? + else + entry [ _col ] = ''; + } infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); } diff --git a/prefs.js b/prefs.js index a873679..9916e0f 100644 --- a/prefs.js +++ b/prefs.js @@ -57,6 +57,7 @@ const prefs = [ { type: Gtk.ColorButton, category: 'Colors', label: _('Warning color'), key: 'warning-fg', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Critical color'), key: 'critical-fg', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown color'), key: 'unknown-fg', align: Gtk.Align.START }, + { type: Gtk.Switch, category: 'Filters', label: _('Do not display acknowledged services'), key: 'acknowledged-filter-out', align: Gtk.Align.START }, { type: Gtk.Entry, category: 'Filters', label: _('Only display hosts matching'), key: 'host-grep' }, { type: Gtk.Entry, category: 'Filters', label: _('Only display services matching'), key: 'service-grep' }, { type: Gtk.Entry, category: 'Filters', label: _('Only display status info matching'), key: 'status-info-grep' }, @@ -325,6 +326,15 @@ function activateAccountRow ( ) { Gio.SettingsBindFlags.DEFAULT ); } + else if ( prefEntry.type == Gtk.Switch ) + { + _account_settings.bind ( + prefEntry.key, + this.prefWidgets[prefEntry.key], + 'active', + Gio.SettingsBindFlags.DEFAULT + ); + } else if ( prefEntry.type == Gtk.ColorButton ) { let _color = new Gdk.RGBA ( ); diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 361727e..3bcc7ff 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -110,7 +110,7 @@ - ['status','host_name','service_display_name','last_check','attempts','status_information'] + ['status','host_name','service_display_name','has_been_acknowledged','last_check','attempts','status_information'] @@ -129,6 +129,10 @@ '' + + false + + '' diff --git a/servers/genericserver.js b/servers/genericserver.js index 416bae6..8b8e4ec 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -169,14 +169,22 @@ class GenericServer { for ( var _filter of filters ) _filter.value = this._settings.get_string ( _filter.prefKey ); - entries: for ( var i = 0 ; i < status.length ; i ++ ) + entries: + for ( var i = 0 ; i < status.length ; i ++ ) { + if ( status[i]['has_been_acknowledged'] && this._settings.get_boolean ( 'acknowledged-filter-out' ) ) + { + log ( '> ACKED:%s ' . format ( status[i]['service_display_name'] ) ); + status.splice ( i, 1 ); + i --; // This has been removed, so get back one step. + continue entries; + } + for ( var _filter of filters ) { if ( _filter['value'] && ( status[i][_filter['entryKey']].match ( new RegExp ( _filter['value'], 'i' ) ) <= 0 ) == _filter['positive'] ) { -// log ( '> NOT MATCHING: %s =~ %s == %s ' . format ( status[i][_filter['entryKey']], _filter['value'], _filter['positive'] ) ); status.splice ( i, 1 ); i --; // This has been removed, so get back one step. continue entries; diff --git a/servers/icinga2.js b/servers/icinga2.js index 05d2930..8ccc37b 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -71,6 +71,7 @@ class Icinga2 extends GenericServer { host_name: entry.host_name, service_display_name: entry.service_display_name, attempts: entry.service_attempt, + has_been_acknowledged: parseInt(entry.service_acknowledged), status_information: entry.service_output, last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), } ); diff --git a/servers/icinga2api.js b/servers/icinga2api.js index b7ea4c1..b68be7b 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -89,6 +89,7 @@ class Icinga2API extends GenericServer { status: _statuses [ entry.attrs.state ], host_name: entry.attrs.host_name, service_display_name: entry.attrs.display_name, + has_been_acknowledged: parseInt(entry.attrs.acknowledgement), attempts: '%d/%d'.format(entry.attrs.check_attempt,entry.attrs.max_check_attempts), status_information: entry.attrs.last_check_result.output, last_check: new Date ( entry.attrs.last_state_change * 1000 ) . toString(), From 1e2517767338d71a1175b19e257598e27cde444a Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 3 Dec 2021 16:35:50 +0100 Subject: [PATCH 35/55] Implement spinners --- extension.js | 53 +++++++++++++++++++++++++++++++++++----- servers/genericserver.js | 8 +++++- servers/icinga.js | 11 +++------ servers/icinga2.js | 8 +++--- servers/icinga2api.js | 13 +++++----- 5 files changed, 67 insertions(+), 26 deletions(-) diff --git a/extension.js b/extension.js index 2fcdf6a..646303b 100644 --- a/extension.js +++ b/extension.js @@ -39,7 +39,7 @@ const PopupMenu = imports.ui.popupMenu; const Gettext = imports.gettext.domain(GETTEXT_DOMAIN); const _ = Gettext.gettext; -const { GObject, St, Clutter, Gio } = imports.gi; +const { GObject, St, Clutter, Gio, GLib } = imports.gi; const SETTINGS_SCHEMA = "org.gnome.shell.extensions.monito"; const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito.account"; @@ -85,11 +85,11 @@ class Indicator extends PanelMenu.Button { let type = this.account_settings.get_string ( "type" ); if ( type == 'Icinga' ) - this.serverLogic = new Icinga ( this.server ); + this.serverLogic = new Icinga ( this.server, this ); else if ( type == 'Icinga2' ) - this.serverLogic = new Icinga2 ( this.server ); + this.serverLogic = new Icinga2 ( this.server, this ); else if ( type == 'Icinga2API' ) - this.serverLogic = new Icinga2API ( this.server ); + this.serverLogic = new Icinga2API ( this.server, this ); this.initUI ( ); } @@ -196,10 +196,45 @@ class Indicator extends PanelMenu.Button { updateStatus ( ) { - this.serverLogic.refresh ( this ); + this.spinChildOf ( this._reloadButton ); + this.serverLogic.refresh ( ); this.setupTimeout ( ); } + spinChildOf ( widget ) + { + if ( ! widget ) + return; + + widget.prevChild = widget.get_child ( ); + let _size = widget.prevChild.width; + + let _child = new St.Icon({ + style_class: 'monito-button-icon', + icon_name: 'process-working-symbolic', + icon_size: _size, + width: _size, + height: _size, + }); + + let _transition = new Clutter.PropertyTransition ( { property_name: 'rotation_angle_z', + repeat_count: -1, + duration: 1000 } ); + _transition.set_from ( 0 ); + _transition.set_to ( 360 ); + + _child.set_pivot_point ( .5, .5 ); + _child.add_transition ( 'rotation', _transition ); + + widget.set_child ( _child ); + } + + stopChildSpin ( widget ) + { + widget.remove_all_children ( ); + widget.set_child ( widget.prevChild ); + } + createHeaderBin ( colName ) { let col = column_definitions [ colName ]; @@ -442,6 +477,8 @@ class Indicator extends PanelMenu.Button { monitoLog ( e.stack ); } + this.stopChildSpin ( this._reloadButton ); + return; } @@ -478,7 +515,10 @@ class Indicator extends PanelMenu.Button { _onRecheckButtonClick ( e ) { - monitoLog ( JSON.stringify ( e.service ) ); + this.spinChildOf ( e ); +// monitoLog ( JSON.stringify ( e.service ) ); + e.service.button = e; + this.serverLogic.recheck ( e.service ); } @@ -491,6 +531,7 @@ class Indicator extends PanelMenu.Button { track_hover: true, accessible_name: text, style_class: 'button big-button', + rotation_angle_x: 0.0, }); button.child = new St.Icon({ diff --git a/servers/genericserver.js b/servers/genericserver.js index 8b8e4ec..7620040 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -31,7 +31,7 @@ const Preferences = Me.imports.prefs; class GenericServer { - constructor ( _server, _serverType = 'Generic' ) + constructor ( _server, _extension, _serverType = 'Generic' ) { log ( '>>> New %s server #%s'.format ( _serverType, _server ) ); @@ -40,6 +40,8 @@ class GenericServer { this._httpSession = null; this._url = null; + this.extension = _extension; + this.canRecheck = true; } @@ -128,6 +130,10 @@ class GenericServer { log ( e ); log ( _data ); } + + if ( message.button ) + this.extension.stopChildSpin ( message.button ); + } diff --git a/servers/icinga.js b/servers/icinga.js index d64034a..3084190 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -33,13 +33,11 @@ let _httpSession; class Icinga extends GenericServer { - constructor ( _server ) { - super(_server, 'Icinga'); + constructor ( _server, extension ) { + super(_server, extension, 'Icinga'); } - refresh ( extension ) { - this.extension = extension; - + refresh ( ) { this.buildURL ( ); this.prepareHttp ( ); } @@ -75,8 +73,7 @@ class Icinga extends GenericServer { message.request_body.truncate(); message.request_body.append ( params ); message.request_body.flatten(); - - // TODO: change button to spinner + message.button = entry.button; this.authenticateAndSend ( message, this.handleCMDMessage ); } diff --git a/servers/icinga2.js b/servers/icinga2.js index 8ccc37b..5d72b87 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -31,15 +31,13 @@ const GenericServer = Me.imports.servers.genericserver.GenericServer; class Icinga2 extends GenericServer { - constructor ( _server ) { - super(_server, 'Icinga2'); + constructor ( _server, extension ) { + super(_server, extension, 'Icinga2'); this.canRecheck = false; } - refresh ( extension ) { - this.extension = extension; - + refresh ( ) { this.buildURL ( ); this.prepareHttp ( ); } diff --git a/servers/icinga2api.js b/servers/icinga2api.js index b68be7b..60b4394 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -32,13 +32,11 @@ const GenericServer = Me.imports.servers.genericserver.GenericServer; class Icinga2API extends GenericServer { - constructor ( _server ) { - super(_server, 'Icinga2 API'); - } - - refresh ( extension ) { - this.extension = extension; + constructor ( _server, extension ) { + super(_server, extension, 'Icinga2 API'); + } + refresh ( ) { this.buildURL ( ); this.prepareHttp ( ); } @@ -62,8 +60,9 @@ class Icinga2API extends GenericServer { message.request_body.truncate(); message.request_body.append ( params ); message.request_body.flatten(); + message.button = entry.button; - log ( '> Body: ' + message.request_body.data ); +// log ( '> Body: ' + message.request_body.data ); message.request_headers.append ( 'Accept', 'application/json' ); log ( message.request_headers ); From bba8b0a69ce1ac5c5d53d5ba29ff21b5011470d7 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 6 Dec 2021 11:34:55 +0100 Subject: [PATCH 36/55] Implement natural date + fixes --- extension.js | 40 +++++---- prefs.js | 4 +- ...ll.extensions.monito@drieu.org.gschema.xml | 4 +- servers/genericserver.js | 82 +++++++++++++++++++ servers/icinga.js | 4 + servers/icinga2.js | 2 +- servers/icinga2api.js | 22 ++++- 7 files changed, 136 insertions(+), 22 deletions(-) diff --git a/extension.js b/extension.js index 646303b..70ae376 100644 --- a/extension.js +++ b/extension.js @@ -62,8 +62,8 @@ const column_definitions = { has_been_acknowledged: { label: _('Ack'), width: 50, expand: false, align: Clutter.ActorAlign.CENTER, style: 'font-weight:bold;' }, last_check: { label: _('Last check'), width: 200, expand: false, type: 'date' }, attempts: { label: _('Attempts'), width: 50, expand: false, }, - status_information: { label: _('Information'), width: 600, expand: true, }, - actions: { label: 'Actions', width: 100, expand: true, special: 'actions' }, + status_information: { label: _('Information'), width: 600, expand: false, }, + actions: { label: 'Actions', width: 120, expand: false, special: 'actions' }, }; @@ -144,16 +144,19 @@ class Indicator extends PanelMenu.Button { _iconBin.child = _icon; this._buttonMenu.actor.add_actor(_iconBin); - this._mainLabel = new St.Label ( { style_class: 'monito-title', text: 'Monito Checker', x_expand: true } ); + this._mainLabel = new St.Label ( { style_class: 'monito-title', text: _('Monito Checker'), x_expand: true } ); this._buttonMenu.actor.add_actor(this._mainLabel); - this._prefsButton = this._createButton ( 'preferences-system-symbolic', 'Preferences', this._onPreferencesActivate ); + this._prefsButton = this._createButton ( 'preferences-system-symbolic', _('Preferences'), this._onPreferencesActivate ); this._buttonMenu.actor.add_child (this._prefsButton); - this._updateButton = this._createButton ( 'emblem-synchronizing-symbolic', 'Reload', this.updateStatus ); // Implement this - this._buttonMenu.actor.add_child (this._updateButton ); + if ( this.serverLogic && this.serverLogic.canRecheck ) + { + this._recheckButton = this._createButton ( 'system-run-symbolic', _('Recheck all'), this.recheckAll ); + this._buttonMenu.actor.add_child (this._recheckButton ); + } - this._reloadButton = this._createButton ( 'view-refresh-symbolic', 'Reload', this.updateStatus ); + this._reloadButton = this._createButton ( 'view-refresh-symbolic', _('Reload view'), this.updateStatus ); this._buttonMenu.actor.add_child (this._reloadButton ); let _intermediate = new PopupMenu.PopupBaseMenuItem ( { @@ -194,6 +197,12 @@ class Indicator extends PanelMenu.Button { 'UNKNOWN': 0 }; } + recheckAll ( ) + { + this.spinChildOf ( this._recheckButton ); + this.serverLogic.recheckAll ( this._recheckButton ); + } + updateStatus ( ) { this.spinChildOf ( this._reloadButton ); @@ -262,7 +271,7 @@ class Indicator extends PanelMenu.Button { _iconBin.child = this.sortIcons [ colName ]; let _button = new St.Button ( { - x_align: Clutter.ActorAlign.START, + x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.CENTER, width: col.width, reactive: true, @@ -294,24 +303,28 @@ class Indicator extends PanelMenu.Button { if ( ! col [ 'special' ] ) _child = new St.Label ( { style_class: 'monito-label', + width: col.width, text: text.toString(), x_align: ( col.align ? col.align : Clutter.ActorAlign.START ), style: ( col.style ? col.style : '' ) } ); else if ( col.special == 'actions' && this.serverLogic.canRecheck ) { - _child = new St.BoxLayout ( { x_expand: false, - vertical: false } ); + _child = new St.BoxLayout ( { x_expand: true, + vertical: false, + width: col.width, } ); let _button = new St.Button ( { style_class: 'button small-button', x_expand: true, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, + width: 24, + height: 24, can_focus: true, } ); _button.service = text; _button.connect ( 'clicked', Lang.bind ( this, this._onRecheckButtonClick ) ); _button.child = new St.Icon ( { - icon_name: 'view-refresh-symbolic', + icon_name: 'system-run-symbolic', icon_size: 16, width: 16, height: 16, @@ -320,7 +333,6 @@ class Indicator extends PanelMenu.Button { } let _bin = new St.Bin({ -// style: 'background: purple', track_hover: true, width: col.width, x_expand: col.expand, @@ -374,7 +386,7 @@ class Indicator extends PanelMenu.Button { } let headerBox = new St.BoxLayout({ - hover: true, + track_hover: true, x_expand: true }); this._box.add_child(headerBox); @@ -389,7 +401,7 @@ class Indicator extends PanelMenu.Button { this._box.add_child(scrollBox); let tableBox = new St.BoxLayout({ - hover: true, + track_hover: true, vertical: true, x_expand: true, }); diff --git a/prefs.js b/prefs.js index 9916e0f..cd4803b 100644 --- a/prefs.js +++ b/prefs.js @@ -281,8 +281,8 @@ function createPrefWidgets ( noteBook, type, isActive ) else if ( prefEntry.key == 'type' ) { [ { name: 'Icinga', value: 'Icinga server' }, - { name: 'Icinga2', value: 'Icinga2 server (using Icingaweb2)' }, - { name: 'Icinga2API', value: 'Icinga2 server (using API)' } ].forEach((item) => { + { name: 'Icinga2API', value: 'Icinga2 server (using API, prefered)' }, + { name: 'Icinga2', value: 'Icinga2 server (using Icingaweb2, limited)' } ].forEach((item) => { this.prefWidgets[prefEntry.key].insert ( -1, item.name, item.value ); } ); } diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 3bcc7ff..45f9530 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -22,8 +22,8 @@ - - + + diff --git a/servers/genericserver.js b/servers/genericserver.js index 7620040..0b6689e 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -224,4 +224,86 @@ class GenericServer { } return 0; } + + formatDate ( date ) + { + if ( typeof date == 'string' || date instanceof String ) + { + date = Date.parse ( date ) / 1000; + log ( date ); + } + + return this.timeAgo ( new Date ( date * 1000 ) ); + } + + getFormattedDate(date, prefomattedDate = false, hideYear = false) + { + const MONTH_NAMES = [ + 'Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', + 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.' + ]; + + const day = date.getDate(); + const month = MONTH_NAMES[date.getMonth()]; + const year = date.getFullYear(); + const hours = date.getHours(); + let minutes = date.getMinutes(); + + if (minutes < 10) { + // Adding leading zero to minutes + minutes = _(`0${ minutes }`); + } + + if (prefomattedDate) { + // Today at 10:20 + // Yesterday at 10:20 + return _(`${ prefomattedDate } at ${ hours }:${ minutes }`); + } + + if (hideYear) { + // 10. January at 10:20 + return _(`${ month } ${ day } at ${ hours }:${ minutes }`); + } + + // 10. January 2017. at 10:20 + return _(`${ year } ${ month } ${ day }`); + } + + timeAgo ( dateParam ) + { + if (!dateParam) { + return null; + } + + const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam); + const DAY_IN_MS = 86400000; // 24 * 60 * 60 * 1000 + const today = new Date(); + const yesterday = new Date(today - DAY_IN_MS); + const seconds = Math.round((today - date) / 1000); + const minutes = Math.round(seconds / 60); + const isToday = today.toDateString() === date.toDateString(); + const isYesterday = yesterday.toDateString() === date.toDateString(); + const isThisYear = today.getFullYear() === date.getFullYear(); + + + if (seconds < 5) { + return 'now'; + } else if (seconds < 60) { + return `${ seconds } seconds ago`; + } else if (seconds < 90) { + return 'a minute ago'; + } else if (minutes < 60) { + return `${ minutes } minutes ago`; + } else if (isToday) { + return this.getFormattedDate(date, 'today'); // Today at 10:20 + } else if (isYesterday) { + return this.getFormattedDate(date, 'yesterday'); // Yesterday at 10:20 + } else if (isThisYear) { + return this.getFormattedDate(date, false, true); // 10. January at 10:20 + } + + return this.getFormattedDate(date); // 10. January 2017. at 10:20 + } + + } diff --git a/servers/icinga.js b/servers/icinga.js index 3084190..8af93fd 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -88,7 +88,11 @@ class Icinga extends GenericServer { else { let json = JSON.parse ( _data ); + this.status = json.status; + log ( this.status ); + for ( var entry of this.status.service_status ) + entry.last_check = this.formatDate ( entry.last_check ); } this.extension.refreshUI ( this ); diff --git a/servers/icinga2.js b/servers/icinga2.js index 5d72b87..f90fed0 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -71,7 +71,7 @@ class Icinga2 extends GenericServer { attempts: entry.service_attempt, has_been_acknowledged: parseInt(entry.service_acknowledged), status_information: entry.service_output, - last_check: new Date ( parseInt(entry.service_last_state_change) * 1000 ) . toString(), + last_check: this.formatDate(parseInt(entry.service_last_state_change)), } ); } } diff --git a/servers/icinga2api.js b/servers/icinga2api.js index 60b4394..b884fa3 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -62,10 +62,25 @@ class Icinga2API extends GenericServer { message.request_body.flatten(); message.button = entry.button; -// log ( '> Body: ' + message.request_body.data ); + message.request_headers.append ( 'Accept', 'application/json' ); +// log ( message.request_headers ); + + this.authenticateAndSend ( message, this.handleCMDMessage ); + } + + recheckAll ( button ) + { + let message = Soup.form_request_new_from_hash ( 'POST', this.urlcgi + '/actions/reschedule-check', { } ); + + let params = '{ "type": "Service", "force": true, "pretty": true }'; + + message.request_body.truncate(); + message.request_body.append ( params ); + message.request_body.flatten(); + message.button = button; message.request_headers.append ( 'Accept', 'application/json' ); - log ( message.request_headers ); +// log ( message.request_headers ); this.authenticateAndSend ( message, this.handleCMDMessage ); } @@ -84,6 +99,7 @@ class Icinga2API extends GenericServer { for ( var entry of json.results ) { +// log ( JSON.stringify(entry) ); this.status.service_status.push ( { status: _statuses [ entry.attrs.state ], host_name: entry.attrs.host_name, @@ -91,7 +107,7 @@ class Icinga2API extends GenericServer { has_been_acknowledged: parseInt(entry.attrs.acknowledgement), attempts: '%d/%d'.format(entry.attrs.check_attempt,entry.attrs.max_check_attempts), status_information: entry.attrs.last_check_result.output, - last_check: new Date ( entry.attrs.last_state_change * 1000 ) . toString(), + last_check: this.formatDate(parseInt(entry.attrs.last_check)), } ); } } From 44d2c57dc4361748e20b51f4aa62fe90a1b9db9e Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 6 Dec 2021 11:45:02 +0100 Subject: [PATCH 37/55] Better handling of spinners --- extension.js | 20 ++++++++++++++------ servers/genericserver.js | 3 --- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/extension.js b/extension.js index 70ae376..7912370 100644 --- a/extension.js +++ b/extension.js @@ -215,8 +215,7 @@ class Indicator extends PanelMenu.Button { if ( ! widget ) return; - widget.prevChild = widget.get_child ( ); - let _size = widget.prevChild.width; + let _size = widget.get_child().width; let _child = new St.Icon({ style_class: 'monito-button-icon', @@ -232,16 +231,24 @@ class Indicator extends PanelMenu.Button { _transition.set_from ( 0 ); _transition.set_to ( 360 ); + widget.set_child ( _child ); _child.set_pivot_point ( .5, .5 ); _child.add_transition ( 'rotation', _transition ); - - widget.set_child ( _child ); } stopChildSpin ( widget ) { - widget.remove_all_children ( ); - widget.set_child ( widget.prevChild ); + let _size = widget.get_child().width; + + widget.get_child().get_transition('rotation').stop(); + + widget.child = new St.Icon({ + style_class: 'monito-button-icon', + icon_name: widget.prevIcon, + icon_size: _size, + width: _size, + height: _size, + }); } createHeaderBin ( colName ) { @@ -545,6 +552,7 @@ class Indicator extends PanelMenu.Button { style_class: 'button big-button', rotation_angle_x: 0.0, }); + button.prevIcon = icon; button.child = new St.Icon({ style_class: 'monito-button-icon', diff --git a/servers/genericserver.js b/servers/genericserver.js index 0b6689e..e55950e 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -228,10 +228,7 @@ class GenericServer { formatDate ( date ) { if ( typeof date == 'string' || date instanceof String ) - { date = Date.parse ( date ) / 1000; - log ( date ); - } return this.timeAgo ( new Date ( date * 1000 ) ); } From d5f975ce44eebd207ed26eb7b1586700dbe29be6 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 17 Dec 2021 15:18:02 +0100 Subject: [PATCH 38/55] Add pending status + trim status info after first line --- extension.js | 5 +++-- prefs.js | 2 ++ ...rg.gnome.shell.extensions.monito@drieu.org.gschema.xml | 8 ++++++++ servers/icinga2api.js | 8 ++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/extension.js b/extension.js index 7912370..948b17c 100644 --- a/extension.js +++ b/extension.js @@ -63,7 +63,7 @@ const column_definitions = { last_check: { label: _('Last check'), width: 200, expand: false, type: 'date' }, attempts: { label: _('Attempts'), width: 50, expand: false, }, status_information: { label: _('Information'), width: 600, expand: false, }, - actions: { label: 'Actions', width: 120, expand: false, special: 'actions' }, + actions: { label: 'Actions', width: 50, expand: false, special: 'actions' }, }; @@ -328,6 +328,7 @@ class Indicator extends PanelMenu.Button { height: 24, can_focus: true, } ); + _button.prevIcon = 'system-run-symbolic'; _button.service = text; _button.connect ( 'clicked', Lang.bind ( this, this._onRecheckButtonClick ) ); _button.child = new St.Icon ( { @@ -445,7 +446,7 @@ class Indicator extends PanelMenu.Button { this.account_settings.get_string ( 'service-replace' ) ); else if ( _col == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), - this.account_settings.get_string ( 'status-info-replace' ) ); + this.account_settings.get_string ( 'status-info-replace' ) ) . replace ( new RegExp ( "\n.*" ), "" ); else if ( _col == 'has_been_acknowledged' ) { if ( entry [ _col ] ) diff --git a/prefs.js b/prefs.js index cd4803b..313d5f2 100644 --- a/prefs.js +++ b/prefs.js @@ -53,10 +53,12 @@ const prefs = [ { type: Gtk.ColorButton, category: 'Colors', label: _('Warning background color'), key: 'warning-color', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Critical background color'), key: 'critical-color', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown background color'), key: 'unknown-color', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Pending background color'), key: 'pending-color', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('OK color'), key: 'ok-fg', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Warning color'), key: 'warning-fg', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Critical color'), key: 'critical-fg', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Unknown color'), key: 'unknown-fg', align: Gtk.Align.START }, + { type: Gtk.ColorButton, category: 'Colors', label: _('Pending color'), key: 'pending-fg', align: Gtk.Align.START }, { type: Gtk.Switch, category: 'Filters', label: _('Do not display acknowledged services'), key: 'acknowledged-filter-out', align: Gtk.Align.START }, { type: Gtk.Entry, category: 'Filters', label: _('Only display hosts matching'), key: 'host-grep' }, { type: Gtk.Entry, category: 'Filters', label: _('Only display services matching'), key: 'service-grep' }, diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 45f9530..93aab33 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -93,6 +93,10 @@ '#e496f5' + + '#aaaaaa' + + '#ffffff' @@ -109,6 +113,10 @@ '#ffffff' + + '#ffffff' + + ['status','host_name','service_display_name','has_been_acknowledged','last_check','attempts','status_information'] diff --git a/servers/icinga2api.js b/servers/icinga2api.js index b884fa3..54ef4ad 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -95,19 +95,19 @@ class Icinga2API extends GenericServer { else { let json = JSON.parse ( _data ); - let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ]; + let _statuses = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN', 'PENDING' ]; for ( var entry of json.results ) { // log ( JSON.stringify(entry) ); this.status.service_status.push ( { - status: _statuses [ entry.attrs.state ], + status: _statuses[entry.attrs.state], host_name: entry.attrs.host_name, service_display_name: entry.attrs.display_name, has_been_acknowledged: parseInt(entry.attrs.acknowledgement), attempts: '%d/%d'.format(entry.attrs.check_attempt,entry.attrs.max_check_attempts), - status_information: entry.attrs.last_check_result.output, - last_check: this.formatDate(parseInt(entry.attrs.last_check)), + status_information: ( entry.attrs.last_check_result ? entry.attrs.last_check_result.output : _statuses[entry.attrs.state] ), + last_check: ( entry.attrs.last_check ? this.formatDate(parseInt(entry.attrs.last_check)) : '' ), } ); } } From 191c1bcb21db23e060bb0ac5152a782588ec37e2 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 11 Feb 2022 21:20:36 +0100 Subject: [PATCH 39/55] Upadte for newer gnome-shell --- extension.js | 2 ++ metadata.json | 2 +- prefs.js | 2 +- servers/genericserver.js | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/extension.js b/extension.js index 7912370..6da2284 100644 --- a/extension.js +++ b/extension.js @@ -389,6 +389,7 @@ class Indicator extends PanelMenu.Button { this.boxes['warning'].set_text ( '…' ); this.boxes['critical'].set_text ( '…' ); this.boxes['unknown'].set_text ( '…' ); + this.stopChildSpin ( this._reloadButton ); return; } @@ -498,6 +499,7 @@ class Indicator extends PanelMenu.Button { this.stopChildSpin ( this._reloadButton ); + monitoLog ( 'RefreshUI done' ); return; } diff --git a/metadata.json b/metadata.json index fb5d4a3..7dfcff8 100644 --- a/metadata.json +++ b/metadata.json @@ -3,6 +3,6 @@ "description": "Checks for various monitoring servers (Icinga & Icinga2 at the moment)", "uuid": "monito@drieu.org", "shell-version": [ - "3.38" + "40" ] } diff --git a/prefs.js b/prefs.js index cd4803b..6abb051 100644 --- a/prefs.js +++ b/prefs.js @@ -93,7 +93,7 @@ function buildPrefsWidget() { let mainWidget = new Gtk.Notebook( { } ); let prefsWidget = new Gtk.Grid({ - margin: 18, +// margin: 18, column_spacing: 12, row_spacing: 12, column_homogeneous: false, diff --git a/servers/genericserver.js b/servers/genericserver.js index e55950e..0e9b1c4 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -97,7 +97,7 @@ class GenericServer { if ( message.status_code != Soup.Status.OK ) { log ( '>>> Error: ' + message.reason_phrase ); - log ( '>>> Data: ' + message.data ); + //log ( '>>> Data: ' + message.data ); // TODO: add pref for that // Main.notifyError ( 'Monito: ' + this.name, // 'URL: ' + this.urlcgi + "\n" + From e52a346369c6d65303655de7c9a15dbf1f0d3eac Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 21 Mar 2022 12:10:05 +0100 Subject: [PATCH 40/55] Be less strict on SSL: add an option instead? --- servers/genericserver.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/servers/genericserver.js b/servers/genericserver.js index e55950e..9375add 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -52,14 +52,24 @@ class GenericServer { buildURL ( ) { - if ( ! this._settings ) - this._settings = Preferences.getAccountSettings ( this._server ); +// if ( ! this._settings ) + this._settings = Preferences.getAccountSettings ( this._server ); - this.type = this._settings.get_string ( "type" ); - this.username = this._settings.get_string ( "username" ); - this.name = this._settings.get_string ( "name" ); - this.password = this._settings.get_string ( "password" ); - this.urlcgi = this._settings.get_string ( "urlcgi" ); + if ( this.type != this._settings.get_string ( "type" ) || + this.username != this._settings.get_string ( "username" ) || + this.strict_ssl != this._settings.get_boolean ( "strict-ssl" ) || + this.name != this._settings.get_string ( "name" ) || + this.password != this._settings.get_string ( "password" ) || + this.urlcgi != this._settings.get_string ( "urlcgi" ) ) + { + this.type = this._settings.get_string ( "type" ); + this.username = this._settings.get_string ( "username" ); + this.strict_ssl = this._settings.get_boolean ( "strict-ssl" ); + this.name = this._settings.get_string ( "name" ); + this.password = this._settings.get_string ( "password" ); + this.urlcgi = this._settings.get_string ( "urlcgi" ); + this._httpSession = null; + } // log ( 'monito server >>> ' + this._server ); // log ( 'monito name >>> ' + this.name ); @@ -73,6 +83,7 @@ class GenericServer { { if ( this._httpSession == null ) { this._httpSession = new Soup.Session(); + this._httpSession.ssl_strict = this.strict_ssl; this._httpSession.user_agent = Me.metadata.uuid; } } From 5e8e660c7aba15eecce2980886310cf0ce752880 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Tue, 22 Mar 2022 22:19:07 +0100 Subject: [PATCH 41/55] Updates --- extension.js | 47 +++++++++++++++++-- prefs.js | 2 + ...ll.extensions.monito@drieu.org.gschema.xml | 8 ++++ servers/genericserver.js | 13 +++++ servers/icinga2api.js | 2 + stylesheet.css | 7 +++ 6 files changed, 74 insertions(+), 5 deletions(-) diff --git a/extension.js b/extension.js index 948b17c..33892cb 100644 --- a/extension.js +++ b/extension.js @@ -147,12 +147,32 @@ class Indicator extends PanelMenu.Button { this._mainLabel = new St.Label ( { style_class: 'monito-title', text: _('Monito Checker'), x_expand: true } ); this._buttonMenu.actor.add_actor(this._mainLabel); + this._searchField = new St.Entry ( { + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.CENTER, + reactive: true, + can_focus: true, + track_hover: true, + style_class: 'entry', + secondary_icon: new St.Icon ( { + style_class: 'monito-button-icon', + icon_name: 'edit-find-symbolic', + icon_size: 24, + width: 24, + height: 24, + } ), + }); + this._searchField.connect('secondary-icon-clicked', Lang.bind(this, this._onSearchFieldActivate ) ); + this._searchField.clutter_text.connect('activate', Lang.bind(this, this._onSearchFieldActivate ) ); + + this._buttonMenu.actor.add_child (this._searchField); + this._prefsButton = this._createButton ( 'preferences-system-symbolic', _('Preferences'), this._onPreferencesActivate ); this._buttonMenu.actor.add_child (this._prefsButton); if ( this.serverLogic && this.serverLogic.canRecheck ) { - this._recheckButton = this._createButton ( 'system-run-symbolic', _('Recheck all'), this.recheckAll ); + this._recheckButton = this._createButton ( 'mail-send-receive-symbolic', _('Recheck all'), this.recheckAll ); this._buttonMenu.actor.add_child (this._recheckButton ); } @@ -328,11 +348,11 @@ class Indicator extends PanelMenu.Button { height: 24, can_focus: true, } ); - _button.prevIcon = 'system-run-symbolic'; + _button.prevIcon = 'mail-send-receive-symbolic'; _button.service = text; _button.connect ( 'clicked', Lang.bind ( this, this._onRecheckButtonClick ) ); _button.child = new St.Icon ( { - icon_name: 'system-run-symbolic', + icon_name: 'mail-send-receive-symbolic', icon_size: 16, width: 16, height: 16, @@ -422,6 +442,12 @@ class Indicator extends PanelMenu.Button { let _row = 0; for ( let entry of processedStatus ) { + if ( this._searchString && + ! ( entry [ 'host_name' ].includes ( this._searchString ) || + entry [ 'service_display_name' ].includes ( this._searchString ) || + entry [ 'status_information' ].includes ( this._searchString ) ) ) + continue; + if ( ( ! _status [ 'WARNING' ] && ! _status [ 'CRITICAL' ] && ! _status [ 'UNKNOWN' ] && entry.status == 'OK' ) || ( ( _status [ 'WARNING' ] || _status [ 'CRITICAL' ] || _status [ 'UNKNOWN' ] ) && entry.status != 'OK' ) ) { @@ -502,6 +528,13 @@ class Indicator extends PanelMenu.Button { return; } + _onSearchFieldActivate ( e ) { + monitoLog ( 'Search: ' + e.text ); + this._searchString = e.text; + this.refreshUI ( ); + return; + } + _onPreferencesActivate ( ) { this.menu.actor.hide(); if (typeof ExtensionUtils.openPrefs === 'function') { @@ -587,8 +620,12 @@ class Extension { for ( let _server of Preferences.getServersList() ) { - this._indicators [ _server ] = new Indicator(_server); - Main.panel.addToStatusArea ( '%s-%d'.format ( this._uuid, _server), this._indicators [ _server ] ); + let _pref = Preferences.getAccountSettings ( _server ); + if ( _pref.get_boolean ( 'active' ) ) + { + this._indicators [ _server ] = new Indicator(_server); + Main.panel.addToStatusArea ( '%s-%d'.format ( this._uuid, _server), this._indicators [ _server ] ); + } } } diff --git a/prefs.js b/prefs.js index 313d5f2..5b424e6 100644 --- a/prefs.js +++ b/prefs.js @@ -44,11 +44,13 @@ const SETTINGS_SCHEMA_ACCOUNT_PATH = "/org/gnome/shell/extensions/monito/account const prefs = [ + { type: Gtk.Switch, category: 'Settings', label: _('Active (restart for effect)'), key: 'active', align: Gtk.Align.START }, { type: Gtk.Entry, category: 'Settings', label: _('Name'), key: 'name' }, { type: Gtk.ComboBoxText, category: 'Settings', label: _('Type'), key: 'type' }, { type: Gtk.Entry, category: 'Settings', label: _('Username'), key: 'username' }, { type: Gtk.Entry, category: 'Settings', label: _('Password'), key: 'password' }, { type: Gtk.Entry, category: 'Settings', label: _('URL CGI'), key: 'urlcgi' }, + { type: Gtk.Switch, category: 'Settings', label: _('Check SSL certificate'), key: 'strict-ssl', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('OK background color'), key: 'ok-color', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Warning background color'), key: 'warning-color', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Critical background color'), key: 'critical-color', align: Gtk.Align.START }, diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 93aab33..dc45f34 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -53,6 +53,10 @@ + + true + + '' @@ -77,6 +81,10 @@ '' + + true + + '#00cc33' diff --git a/servers/genericserver.js b/servers/genericserver.js index 9375add..ceec9bf 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -53,6 +53,8 @@ class GenericServer { buildURL ( ) { // if ( ! this._settings ) + log ( 'monito build URL' ); + this._settings = Preferences.getAccountSettings ( this._server ); if ( this.type != this._settings.get_string ( "type" ) || @@ -69,6 +71,7 @@ class GenericServer { this.password = this._settings.get_string ( "password" ); this.urlcgi = this._settings.get_string ( "urlcgi" ); this._httpSession = null; + log ( 'Refreshing URL parameters' ); } // log ( 'monito server >>> ' + this._server ); @@ -82,6 +85,7 @@ class GenericServer { prepareHttp ( ) { if ( this._httpSession == null ) { + log ( 'Preparing new HTTP with strict SSL ' + this.strict_ssl ); this._httpSession = new Soup.Session(); this._httpSession.ssl_strict = this.strict_ssl; this._httpSession.user_agent = Me.metadata.uuid; @@ -95,6 +99,7 @@ class GenericServer { auth.authenticate ( this.username, this.password ); message.request_headers.append ( "Authorization", auth.get_authorization ( message ) ); + log ( 'Sending message' ); this._httpSession.queue_message ( message, Lang.bind (this, callback ) ); } @@ -105,6 +110,14 @@ class GenericServer { this.status.service_status = [ ]; this.error = null; + log ( message.status_code ); + log ( message.response_body ); + message.response_headers.foreach ((name, val) => { + log (name, val); + }); + log ( message.response_body.data ); + + if ( message.status_code != Soup.Status.OK ) { log ( '>>> Error: ' + message.reason_phrase ); diff --git a/servers/icinga2api.js b/servers/icinga2api.js index 54ef4ad..b460cd1 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -87,6 +87,8 @@ class Icinga2API extends GenericServer { handlePollMessage ( _httpSession, message ) { + log ( 'handlePollMessage' ); + let _data = super.handleMessage ( _httpSession, message ); try { diff --git a/stylesheet.css b/stylesheet.css index fd7d475..1a58019 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -91,7 +91,14 @@ margin: 0px; } +.entry { + margin-right: 0px; + min-width: 256px; + padding: 12px !important; +} + .big-button { + margin-left: 0px; padding: 12px !important; } From 51bfedd34936124d084d60f9124a7b62c84cdb3e Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 22 Apr 2022 11:22:25 +0200 Subject: [PATCH 42/55] Make specific recheck work again --- servers/icinga2api.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/servers/icinga2api.js b/servers/icinga2api.js index b460cd1..960094f 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -55,7 +55,7 @@ class Icinga2API extends GenericServer { { let message = Soup.form_request_new_from_hash ( 'POST', this.urlcgi + '/actions/reschedule-check', { } ); - let params = '{ "type": "Service", "filter": "host.name==\\"%s\\" && service.name==\\"%s\\"", "force": true, "pretty": true }' . format ( encodeURI ( entry.real_host_name ), encodeURI ( entry.real_service_display_name ) ); + let params = '{ "type": "Service", "filter": "host.name==\\"%s\\" && service.name==\\"%s\\"", "force": true, "pretty": true }' . format ( entry.real_host_name, entry.real_service_display_name ); message.request_body.truncate(); message.request_body.append ( params ); @@ -63,7 +63,6 @@ class Icinga2API extends GenericServer { message.button = entry.button; message.request_headers.append ( 'Accept', 'application/json' ); -// log ( message.request_headers ); this.authenticateAndSend ( message, this.handleCMDMessage ); } @@ -80,7 +79,6 @@ class Icinga2API extends GenericServer { message.button = button; message.request_headers.append ( 'Accept', 'application/json' ); -// log ( message.request_headers ); this.authenticateAndSend ( message, this.handleCMDMessage ); } From 5aee2cb9f2085884eef7c4ab0e87af71bce53f6e Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 22 Apr 2022 11:22:38 +0200 Subject: [PATCH 43/55] Be case insenstitive --- extension.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extension.js b/extension.js index 4255397..0638038 100644 --- a/extension.js +++ b/extension.js @@ -164,6 +164,7 @@ class Indicator extends PanelMenu.Button { }); this._searchField.connect('secondary-icon-clicked', Lang.bind(this, this._onSearchFieldActivate ) ); this._searchField.clutter_text.connect('activate', Lang.bind(this, this._onSearchFieldActivate ) ); + this._searchField.clutter_text.connect('text-changed', Lang.bind(this, this._onSearchFieldActivate ) ); this._buttonMenu.actor.add_child (this._searchField); @@ -444,9 +445,9 @@ class Indicator extends PanelMenu.Button { for ( let entry of processedStatus ) { if ( this._searchString && - ! ( entry [ 'host_name' ].includes ( this._searchString ) || - entry [ 'service_display_name' ].includes ( this._searchString ) || - entry [ 'status_information' ].includes ( this._searchString ) ) ) + ! ( entry [ 'host_name' ].toLowerCase().includes ( this._searchString ) || + entry [ 'service_display_name' ].toLowerCase().includes ( this._searchString ) || + entry [ 'status_information' ].toLowerCase().includes ( this._searchString ) ) ) continue; if ( ( ! _status [ 'WARNING' ] && ! _status [ 'CRITICAL' ] && ! _status [ 'UNKNOWN' ] && entry.status == 'OK' ) || @@ -532,7 +533,7 @@ class Indicator extends PanelMenu.Button { _onSearchFieldActivate ( e ) { monitoLog ( 'Search: ' + e.text ); - this._searchString = e.text; + this._searchString = e.text.toLowerCase(); this.refreshUI ( ); return; } From b468ca03267c8c7ccf04e0c446d0ca7d95192692 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 11 May 2022 12:08:41 +0200 Subject: [PATCH 44/55] - Implement Hover - Implement services lines fold/unfold - Implement new action for services lines : launche browser - Some refactoring --- extension.js | 139 +++++++++++++----- prefs.js | 30 +++- ...ll.extensions.monito@drieu.org.gschema.xml | 8 + servers/genericserver.js | 7 +- servers/icinga2.js | 5 + servers/icinga2api.js | 6 + stylesheet.css | 4 + 7 files changed, 156 insertions(+), 43 deletions(-) diff --git a/extension.js b/extension.js index 0638038..6dc7b27 100644 --- a/extension.js +++ b/extension.js @@ -168,16 +168,16 @@ class Indicator extends PanelMenu.Button { this._buttonMenu.actor.add_child (this._searchField); - this._prefsButton = this._createButton ( 'preferences-system-symbolic', _('Preferences'), this._onPreferencesActivate ); + this._prefsButton = this._createButton ( 'big', 'preferences-system-symbolic', _('Preferences'), this._onPreferencesActivate ); this._buttonMenu.actor.add_child (this._prefsButton); if ( this.serverLogic && this.serverLogic.canRecheck ) { - this._recheckButton = this._createButton ( 'mail-send-receive-symbolic', _('Recheck all'), this.recheckAll ); + this._recheckButton = this._createButton ( 'big', 'mail-send-receive-symbolic', _('Recheck all'), this.recheckAll ); this._buttonMenu.actor.add_child (this._recheckButton ); } - this._reloadButton = this._createButton ( 'view-refresh-symbolic', _('Reload view'), this.updateStatus ); + this._reloadButton = this._createButton ( 'big', 'view-refresh-symbolic', _('Reload view'), this.updateStatus ); this._buttonMenu.actor.add_child (this._reloadButton ); let _intermediate = new PopupMenu.PopupBaseMenuItem ( { @@ -330,35 +330,31 @@ class Indicator extends PanelMenu.Button { text = '…'; if ( ! col [ 'special' ] ) + { _child = new St.Label ( { style_class: 'monito-label', + reactive: true, + can_focus: true, + track_hover: true, width: col.width, - text: text.toString(), + text: text.toString().replace(/\n.*/s, ''), x_align: ( col.align ? col.align : Clutter.ActorAlign.START ), style: ( col.style ? col.style : '' ) } ); - else if ( col.special == 'actions' && this.serverLogic.canRecheck ) + _child.original_text = text.toString(); + _child.connect('button-press-event', Lang.bind(this, this._onExpandLabel ) ); + _child.connect('notify::hover', Lang.bind(this, this._onEnterEvent ) ); + } + else if ( col.special == 'actions' ) { _child = new St.BoxLayout ( { x_expand: true, vertical: false, width: col.width, } ); - let _button = new St.Button ( { - style_class: 'button small-button', - x_expand: true, - x_align: Clutter.ActorAlign.END, - y_align: Clutter.ActorAlign.CENTER, - width: 24, - height: 24, - can_focus: true, - } ); - _button.prevIcon = 'mail-send-receive-symbolic'; - _button.service = text; - _button.connect ( 'clicked', Lang.bind ( this, this._onRecheckButtonClick ) ); - _button.child = new St.Icon ( { - icon_name: 'mail-send-receive-symbolic', - icon_size: 16, - width: 16, - height: 16, - } ); - _child.add_child ( _button ); + + if ( this.serverLogic.canRecheck ) + { + _child.add_child ( this._createButton ( 'small', 'mail-send-receive-symbolic', text, this._onRecheckButtonClick ) ); + } + _child.add_child ( this._createButton ( 'small', 'web-browser-symbolic', text, this._onOpenInBrowser ) ); + } let _bin = new St.Bin({ @@ -457,8 +453,10 @@ class Indicator extends PanelMenu.Button { let infoBox = new St.BoxLayout({ style_class: 'monito-service-line', style: _style, - track_hover: true, x_expand: true, + reactive: true, + can_focus: true, + track_hover: true, }); tableBox.add_child(infoBox); @@ -485,8 +483,7 @@ class Indicator extends PanelMenu.Button { infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); } - if ( this.serverLogic.canRecheck ) - infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); + infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); _row ++; } @@ -578,25 +575,101 @@ class Indicator extends PanelMenu.Button { this.serverLogic.recheck ( e.service ); } - _createButton ( icon, text, callback ) { + _onExpandLabel ( e ) + { + log ( "Monito: Click label " + e.original_text ); + let temp = e.text; + e.text = e.original_text; + e.original_text = temp; + if ( e.clutter_text.line_wrap ) + { + e.clutter_text.line_wrap = false; + } + else + { + e.clutter_text.line_wrap = true; + e.clutter_text.line_wrap_mode = Pango.WrapMode.WORD; + e.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + } + + } + + _onEnterEvent ( e ) + { + log ( "Monito: enter"); + } + + _onOpenInBrowser ( e ) + { + this.spinChildOf ( e ); + e.service.button = e; + + let loop = GLib.MainLoop.new(null, false); + try { + let _cmd = "%s %s".format(settings.get_string ( "web-browser" ), this.serverLogic.getUrlForService ( e.service ) ); + let proc = Gio.Subprocess.new ( + _cmd.split ( ' ' ), + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE + ); + + let cancellable = new Gio.Cancellable(); + proc.wait_async(cancellable, (proc, result) => { + try { + proc.wait_finish(result); + + if (proc.get_successful()) { + log ( 'Monito: the process succeeded'); + } else { + log ( 'Monito: the process failed' ); + } + this.stopChildSpin ( e.service.button ); + + } catch (e) { + logError(e); + } finally { + loop.quit(); + } + }); + log ( 'Monito: ' + proc ); + } catch ( e ) { + log ( 'Monito: ' + e ); + } + } + + _createButton ( sizeName, icon, data, callback ) { + let size = 24; + if ( sizeName == 'small' ) + size = 24; + else if ( sizeName == 'big' ) + size = 32; + + let _text = ''; + if ( ! data instanceof Object ) + _text = data; + let button = new St.Button({ x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, reactive: true, can_focus: true, track_hover: true, - accessible_name: text, - style_class: 'button big-button', + accessible_name: _text, + style_class: 'button %s-button'.format(sizeName), rotation_angle_x: 0.0, +// width: size, +// height: size }); button.prevIcon = icon; + if ( data instanceof Object ) + button.service = data; + button.child = new St.Icon({ style_class: 'monito-button-icon', icon_name: icon, - icon_size: 24, - width: 24, - height: 24, + icon_size: size - 4, + width: size - 4, + height: size - 4 }); button.connect('clicked', Lang.bind(this, callback ) ); diff --git a/prefs.js b/prefs.js index 41e8a0a..9280fcc 100644 --- a/prefs.js +++ b/prefs.js @@ -49,7 +49,8 @@ const prefs = [ { type: Gtk.ComboBoxText, category: 'Settings', label: _('Type'), key: 'type' }, { type: Gtk.Entry, category: 'Settings', label: _('Username'), key: 'username' }, { type: Gtk.Entry, category: 'Settings', label: _('Password'), key: 'password' }, - { type: Gtk.Entry, category: 'Settings', label: _('URL CGI'), key: 'urlcgi' }, + { type: Gtk.Entry, category: 'Settings', label: _('Web URL'), key: 'url' }, + { type: Gtk.Entry, category: 'Settings', label: _('CGI / API URL'), key: 'urlcgi' }, { type: Gtk.Switch, category: 'Settings', label: _('Check SSL certificate'), key: 'strict-ssl', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('OK background color'), key: 'ok-color', align: Gtk.Align.START }, { type: Gtk.ColorButton, category: 'Colors', label: _('Warning background color'), key: 'warning-color', align: Gtk.Align.START }, @@ -97,7 +98,7 @@ function buildPrefsWidget() { let mainWidget = new Gtk.Notebook( { } ); let prefsWidget = new Gtk.Grid({ -// margin: 18, + margin: 18, column_spacing: 12, row_spacing: 12, column_homogeneous: false, @@ -107,14 +108,14 @@ function buildPrefsWidget() { // Add a simple title and add it to the prefsWidget let title = new Gtk.Label({ - label: `${Me.metadata.name} Preferences`, + label: '' + _('Monito Preferences') + '', halign: Gtk.Align.START, use_markup: true, }); prefsWidget.attach(title, 0, 0, 2, 1); let _label = new Gtk.Label({ - label: 'Poll delay in seconds', + label: _('Poll delay in seconds'), visible: true, halign: Gtk.Align.START, }); @@ -132,16 +133,33 @@ function buildPrefsWidget() { prefsWidget.attach ( _entry, 1, 1, 1, 1 ); this.settings.bind ( 'poll-delay', _entry, 'value', Gio.SettingsBindFlags.DEFAULT ); + + _label = new Gtk.Label({ + label: _('Web browser'), + visible: true, + halign: Gtk.Align.START, + }); + prefsWidget.attach ( _label, 0, 2, 1, 1 ); + + _entry = new Gtk.Entry ({ + halign: Gtk.Align.FILL, + visible: true, + hexpand: true, + can_focus: true, + }); + prefsWidget.attach ( _entry, 1, 2, 1, 1 ); + this.settings.bind ( 'web-browser', _entry, 'text', Gio.SettingsBindFlags.DEFAULT ); + // Misc Settings (TBD) mainWidget.append_page ( prefsWidget, - new Gtk.Label ( { label: 'General', } ) ); + new Gtk.Label ( { label: _('General'), } ) ); // Accounts this._accountsWidgetContainer = new Gtk.Box( { orientation: Gtk.Orientation.HORIZONTAL, homogeneous: false, } ); mainWidget.append_page ( this._accountsWidgetContainer, - new Gtk.Label ( { label: 'Servers', } ) ); + new Gtk.Label ( { label: _('Servers'), } ) ); let accountsChooserContainer = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL, homogeneous: false, } ); diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index dc45f34..0f0d9e0 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -48,6 +48,10 @@ 300 + + "/usr/bin/firefox" + + @@ -77,6 +81,10 @@ '' + + '' + + '' diff --git a/servers/genericserver.js b/servers/genericserver.js index 9b8de32..a29b899 100644 --- a/servers/genericserver.js +++ b/servers/genericserver.js @@ -110,14 +110,13 @@ class GenericServer { this.status.service_status = [ ]; this.error = null; - log ( message.status_code ); - log ( message.response_body ); +// log ( message.status_code ); +// log ( message.response_body ); message.response_headers.foreach ((name, val) => { log (name, val); }); - log ( message.response_body.data ); +// log ( message.response_body.data ); - if ( message.status_code != Soup.Status.OK ) { log ( '>>> Error: ' + message.reason_phrase ); diff --git a/servers/icinga2.js b/servers/icinga2.js index f90fed0..9b926a6 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -79,5 +79,10 @@ class Icinga2 extends GenericServer { this.extension.refreshUI ( this ); return ! this.error; } + + getUrlForService ( service ) + { + return this._settings.get_string ( 'url' ) ; // + monitoring/service/show?host=XXX&service=XXX + } } diff --git a/servers/icinga2api.js b/servers/icinga2api.js index 960094f..46eb8b0 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -121,5 +121,11 @@ class Icinga2API extends GenericServer { this.extension.refreshUI ( this ); return ! this.error; } + + getUrlForService ( service ) + { + return this._settings.get_string ( 'url' ) ; + } + } diff --git a/stylesheet.css b/stylesheet.css index 1a58019..3d1fa08 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -72,6 +72,10 @@ line-height: 20px; } +.monito-service-line:hover { + font-weight: bold; +} + .monito-title { margin: 2px; font-size: 200%; From b8eba20b7cb7ebf2ff3e70d6064e95b107bba83f Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Wed, 11 May 2022 12:11:11 +0200 Subject: [PATCH 45/55] Update todo --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b1f0ccf..98cf98a 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,10 @@ Things I plan to add at some point or another: * Buttons to operate on services à la nagstamon (recheck, connect via SSH, etc.) * Filters to hide services (handled, scheduled downtime, host down, ...) - * Support for other (as in free speech) monitoring servers + * Support for other free (as in free speech) monitoring servers * Choose which columns are shown in services result Things I will be unlikely to add unless a patch is provided: - * Support for broken HTTPS certs * Notification for new alerts * Display list of down hosts (I am not interested in host monitoring but services) From acc8512800e98280a8c148c29c8713878afde3e2 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 20 May 2022 16:32:20 +0200 Subject: [PATCH 46/55] Start of implementation of custom columns --- prefs.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/prefs.js b/prefs.js index 9280fcc..baab830 100644 --- a/prefs.js +++ b/prefs.js @@ -24,6 +24,7 @@ const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Gtk = imports.gi.Gtk; const Gdk = imports.gi.Gdk; +const GObject = imports.gi.GObject; // It's common practice to keep GNOME API and JS imports in separate blocks const Lang = imports.lang; @@ -245,9 +246,12 @@ function createAccountWidgets ( isActive ) this._accountsWidget = new Gtk.Notebook( { } ); this._accountsWidgetContainer.pack_start(this._accountsWidget, true, true, 0); - for ( var _tab of [ 'Settings', 'Colors', 'Filters', 'Replacements' ] ) + for ( var _tab of [ 'Settings', 'Columns', 'Colors', 'Filters', 'Replacements' ] ) { - this.createPrefWidgets ( _accountsWidget, _tab, isActive ); + if ( _tab != 'Columns' ) + this.createPrefWidgets ( _accountsWidget, _tab, isActive ); + else + this.createColumnsPrefTab ( _accountsWidget, _tab, isActive ); } // Settings @@ -324,6 +328,56 @@ function createPrefWidgets ( noteBook, type, isActive ) } } + +function createColumnsPrefTab ( noteBook, type, isActive ) +{ + let grid = new Gtk.Grid ( { + halign: Gtk.Align.FILL, + margin: 18, + column_spacing: 12, + row_spacing: 12, + visible: true, + column_homogeneous: false, + }); + noteBook.append_page ( grid, new Gtk.Label ( { label: _(type), } ) ); + + let _store = new Gtk.TreeStore(); + _store.set_column_types([GObject.TYPE_STRING, GObject.TYPE_INT]); + _store.filter_new(null); + + let _treeView = new Gtk.TreeView ( { model: _store, + headers_visible: true, + reorderable: true, + hexpand: true, + vexpand: true }); + + let columnNumbers = new Gtk.TreeViewColumn ( { title: _("Column") } ); + let rendererNumbers = new Gtk.CellRendererText ( { editable: true } ); + columnNumbers.pack_start(rendererNumbers, true); + columnNumbers.add_attribute(rendererNumbers, 'text', 0); + _treeView.append_column(columnNumbers); + + let _colSize = new Gtk.TreeViewColumn ( { title: _("Size") } ); + let _colRenderer = new Gtk.CellRendererText ( { editable: true } ); + _colSize.pack_start(_colRenderer, true); + _colSize.add_attribute(_colRenderer, 'text', 1); + _treeView.append_column(_colSize); + + let _item = _store.append(null); + _store.set_value(_item, 0, 'prout' ); + _store.set_value(_item, 1, 2 ); + + _item = _store.append(null); + _store.set_value(_item, 0, 'bar' ); + _store.set_value(_item, 1, 1 ); + +// _treeView.connect('row-activated', this._editPath.bind(this)); + + grid.attach ( _treeView, 0, 1, 1, 1 ); + +} + + function activateAccountRow ( ) { if ( this._accountsWidget ) From 66f1deed14d04ded616923ea5063bf1e5199a53a Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 20 May 2022 16:32:36 +0200 Subject: [PATCH 47/55] Fix URL stuff --- extension.js | 24 ++++++++++-------------- servers/icinga.js | 7 +++++++ servers/icinga2.js | 8 ++++++-- servers/icinga2api.js | 8 +++++--- stylesheet.css | 2 -- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/extension.js b/extension.js index 6dc7b27..11cab08 100644 --- a/extension.js +++ b/extension.js @@ -39,7 +39,7 @@ const PopupMenu = imports.ui.popupMenu; const Gettext = imports.gettext.domain(GETTEXT_DOMAIN); const _ = Gettext.gettext; -const { GObject, St, Clutter, Gio, GLib } = imports.gi; +const { GObject, St, Clutter, Gio, GLib, Pango } = imports.gi; const SETTINGS_SCHEMA = "org.gnome.shell.extensions.monito"; const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito.account"; @@ -341,7 +341,7 @@ class Indicator extends PanelMenu.Button { style: ( col.style ? col.style : '' ) } ); _child.original_text = text.toString(); _child.connect('button-press-event', Lang.bind(this, this._onExpandLabel ) ); - _child.connect('notify::hover', Lang.bind(this, this._onEnterEvent ) ); +// _child.connect('notify::hover', Lang.bind(this, this._onEnterEvent ) ); } else if ( col.special == 'actions' ) { @@ -594,10 +594,10 @@ class Indicator extends PanelMenu.Button { } - _onEnterEvent ( e ) - { - log ( "Monito: enter"); - } +// _onEnterEvent ( e ) +// { +// log ( "Monito: enter"); +// } _onOpenInBrowser ( e ) { @@ -616,24 +616,20 @@ class Indicator extends PanelMenu.Button { proc.wait_async(cancellable, (proc, result) => { try { proc.wait_finish(result); - - if (proc.get_successful()) { - log ( 'Monito: the process succeeded'); - } else { + if ( ! proc.get_successful()) { log ( 'Monito: the process failed' ); } - this.stopChildSpin ( e.service.button ); - } catch (e) { logError(e); } finally { loop.quit(); } }); - log ( 'Monito: ' + proc ); } catch ( e ) { - log ( 'Monito: ' + e ); + log ( 'Monito err: ' + e.message ); + Main.notifyError ( _('Unable to execute command: %s').format ( e.message ) ); } + this.stopChildSpin ( e.service.button ); } _createButton ( sizeName, icon, data, callback ) { diff --git a/servers/icinga.js b/servers/icinga.js index 8af93fd..fe6ed05 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -106,4 +106,11 @@ class Icinga extends GenericServer { } } + getUrlForService ( service ) + { + return '%s/?type=2&host=%s&service=%s'.format ( this._settings.get_string ( 'urlcgi' ).replace ( /status.cgi/, 'extinfo.cgi' ), + encodeURI ( service.real_host_name ), + encodeURI ( service.service_display_name ) ); + } + } diff --git a/servers/icinga2.js b/servers/icinga2.js index 9b926a6..1e924ba 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -82,7 +82,11 @@ class Icinga2 extends GenericServer { getUrlForService ( service ) { - return this._settings.get_string ( 'url' ) ; // + monitoring/service/show?host=XXX&service=XXX - } + log ( service ); + return '%s/monitoring/service/show?host=%s&service=%s'.format ( this._settings.get_string ( 'url' ), + service.host_name, + service.service_display_name ); + } + } diff --git a/servers/icinga2api.js b/servers/icinga2api.js index 46eb8b0..ce9bb79 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -102,6 +102,7 @@ class Icinga2API extends GenericServer { // log ( JSON.stringify(entry) ); this.status.service_status.push ( { status: _statuses[entry.attrs.state], + real_host_name: entry.attrs.host_name, host_name: entry.attrs.host_name, service_display_name: entry.attrs.display_name, has_been_acknowledged: parseInt(entry.attrs.acknowledgement), @@ -124,8 +125,9 @@ class Icinga2API extends GenericServer { getUrlForService ( service ) { - return this._settings.get_string ( 'url' ) ; - } - + return '%s/monitoring/service/show?host=%s&service=%s'.format ( this._settings.get_string ( 'url' ), + encodeURI ( service.real_host_name ), + encodeURI ( service.service_display_name ) ); + } } diff --git a/stylesheet.css b/stylesheet.css index 3d1fa08..d140afa 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -109,5 +109,3 @@ .small-button { padding: 3px !important; } - - From af32fbefd2e1f24f31e2500f605978888988146f Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Sat, 28 May 2022 22:34:01 +0200 Subject: [PATCH 48/55] Port to Gtk4 --- prefs.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/prefs.js b/prefs.js index baab830..f05e724 100644 --- a/prefs.js +++ b/prefs.js @@ -83,7 +83,6 @@ const prefs = [ function init() { monitoLog('initializing ${Me.metadata.name} Preferences'); this.settings = ExtensionUtils.getSettings(SETTINGS_SCHEMA); - monitoLog ( 'Foo: ' + Me.metadata ); } @@ -99,7 +98,7 @@ function buildPrefsWidget() { let mainWidget = new Gtk.Notebook( { } ); let prefsWidget = new Gtk.Grid({ - margin: 18, +// margin: 18, column_spacing: 12, row_spacing: 12, column_homogeneous: false, @@ -158,18 +157,20 @@ function buildPrefsWidget() { // Accounts this._accountsWidgetContainer = new Gtk.Box( { orientation: Gtk.Orientation.HORIZONTAL, - homogeneous: false, } ); + homogeneous: false, } ); mainWidget.append_page ( this._accountsWidgetContainer, new Gtk.Label ( { label: _('Servers'), } ) ); let accountsChooserContainer = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL, homogeneous: false, } ); - this._accountsWidgetContainer.pack_start(accountsChooserContainer, true, true, 0); + monitoLog ( 'Box ' + this._accountsWidgetContainer ); + monitoLog ( 'Func ' + this._accountsWidgetContainer.pack_start ); + this._accountsWidgetContainer.append(accountsChooserContainer, true, true, 0); this.accountsChooser = new Gtk.ListBox ( { valign: Gtk.Align.FILL, hexpand: true, vexpand: true } ); - accountsChooserContainer.pack_start(this.accountsChooser, true, true, 0); + accountsChooserContainer.append(this.accountsChooser, true, true, 0); // Account list for ( var server_id of this.getServersList ( ) ) @@ -178,7 +179,7 @@ function buildPrefsWidget() { // Action Bar let accountsChooserActionBar = new Gtk.ActionBar ( { valign: Gtk.Align.END } ); - accountsChooserContainer.pack_start(accountsChooserActionBar, true, true, 0); + accountsChooserContainer.append(accountsChooserActionBar, true, true, 0); let accountCreateButton = Gtk.Button.new_from_icon_name ( 'list-add', Gtk.IconSize.BUTTON ); @@ -192,7 +193,7 @@ function buildPrefsWidget() { this.createAccountWidgets ( false ) ; - mainVbox.pack_start(mainWidget, true, true, 0); + mainVbox.append(mainWidget, true, true, 0); mainVbox.show_all(); return mainVbox; @@ -244,7 +245,7 @@ function createAccountWidgets ( isActive ) this.prefWidgets = { }; this._accountsWidget = new Gtk.Notebook( { } ); - this._accountsWidgetContainer.pack_start(this._accountsWidget, true, true, 0); + this._accountsWidgetContainer.append(this._accountsWidget, true, true, 0); for ( var _tab of [ 'Settings', 'Columns', 'Colors', 'Filters', 'Replacements' ] ) { @@ -269,7 +270,7 @@ function createPrefWidgets ( noteBook, type, isActive ) { let grid = new Gtk.Grid ( { halign: Gtk.Align.FILL, - margin: 18, +// margin: 18, column_spacing: 12, row_spacing: 12, visible: true, @@ -333,7 +334,7 @@ function createColumnsPrefTab ( noteBook, type, isActive ) { let grid = new Gtk.Grid ( { halign: Gtk.Align.FILL, - margin: 18, +// margin: 18, column_spacing: 12, row_spacing: 12, visible: true, @@ -353,13 +354,13 @@ function createColumnsPrefTab ( noteBook, type, isActive ) let columnNumbers = new Gtk.TreeViewColumn ( { title: _("Column") } ); let rendererNumbers = new Gtk.CellRendererText ( { editable: true } ); - columnNumbers.pack_start(rendererNumbers, true); + columnNumbers.append(rendererNumbers, true); columnNumbers.add_attribute(rendererNumbers, 'text', 0); _treeView.append_column(columnNumbers); let _colSize = new Gtk.TreeViewColumn ( { title: _("Size") } ); let _colRenderer = new Gtk.CellRendererText ( { editable: true } ); - _colSize.pack_start(_colRenderer, true); + _colSize.append(_colRenderer, true); _colSize.add_attribute(_colRenderer, 'text', 1); _treeView.append_column(_colSize); @@ -438,17 +439,16 @@ function addAccountLine ( server_id ) monitoLog ( '> Add line ' + server_id ); let _account_settings = getAccountSettings ( server_id ); let row = new Gtk.ListBoxRow ( { hexpand: true, - halign: Gtk.Align.FILL } ); + halign: Gtk.Align.FILL } ); row.server = server_id; let _label = new Gtk.Label ( { label: _account_settings.get_string('name'), hexpand: true, - margin: 5, - halign: Gtk.Align.START, - expand: true } ); - row.add ( _label ); - this.accountsChooser.add ( row ); - this.accountsChooser.show_all(); +// margin: 5, + halign: Gtk.Align.START } ); + row.set_child ( _label ); + this.accountsChooser.append ( row ); + this.accountsChooser.show_all(); // XXX Does not work in gtk4 ? } From 4e21919d2e5389372c8e0a5406193a8d4d59d03b Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 30 May 2022 11:27:11 +0200 Subject: [PATCH 49/55] Implement GTK+3/4 compatibility --- prefs.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/prefs.js b/prefs.js index f05e724..e25e88c 100644 --- a/prefs.js +++ b/prefs.js @@ -83,6 +83,9 @@ const prefs = [ function init() { monitoLog('initializing ${Me.metadata.name} Preferences'); this.settings = ExtensionUtils.getSettings(SETTINGS_SCHEMA); + + + this.gtkVersion = Gtk.get_major_version(); } @@ -98,12 +101,14 @@ function buildPrefsWidget() { let mainWidget = new Gtk.Notebook( { } ); let prefsWidget = new Gtk.Grid({ -// margin: 18, column_spacing: 12, row_spacing: 12, column_homogeneous: false, }); + if ( this.gtkVersion < 4 ) + prefsWidget.margin = 18; + this.prefWidgets = { }; // Add a simple title and add it to the prefsWidget @@ -165,12 +170,18 @@ function buildPrefsWidget() { homogeneous: false, } ); monitoLog ( 'Box ' + this._accountsWidgetContainer ); monitoLog ( 'Func ' + this._accountsWidgetContainer.pack_start ); - this._accountsWidgetContainer.append(accountsChooserContainer, true, true, 0); + if ( this.gtkVersion == 4 ) + this._accountsWidgetContainer.append(accountsChooserContainer, true, true, 0); + else + this._accountsWidgetContainer.add(accountsChooserContainer, true, true, 0); this.accountsChooser = new Gtk.ListBox ( { valign: Gtk.Align.FILL, hexpand: true, vexpand: true } ); - accountsChooserContainer.append(this.accountsChooser, true, true, 0); + if ( this.gtkVersion == 4 ) + accountsChooserContainer.append(this.accountsChooser, true, true, 0); + else + accountsChooserContainer.add(this.accountsChooser, true, true, 0); // Account list for ( var server_id of this.getServersList ( ) ) @@ -179,7 +190,10 @@ function buildPrefsWidget() { // Action Bar let accountsChooserActionBar = new Gtk.ActionBar ( { valign: Gtk.Align.END } ); - accountsChooserContainer.append(accountsChooserActionBar, true, true, 0); + if ( this.gtkVersion == 4 ) + accountsChooserContainer.append(accountsChooserActionBar, true, true, 0); + else + accountsChooserContainer.add(accountsChooserActionBar, true, true, 0); let accountCreateButton = Gtk.Button.new_from_icon_name ( 'list-add', Gtk.IconSize.BUTTON ); @@ -193,7 +207,10 @@ function buildPrefsWidget() { this.createAccountWidgets ( false ) ; - mainVbox.append(mainWidget, true, true, 0); + if ( this.gtkVersion == 4 ) + mainVbox.append(mainWidget, true, true, 0); + else + mainVbox.add(mainWidget); mainVbox.show_all(); return mainVbox; @@ -245,7 +262,10 @@ function createAccountWidgets ( isActive ) this.prefWidgets = { }; this._accountsWidget = new Gtk.Notebook( { } ); - this._accountsWidgetContainer.append(this._accountsWidget, true, true, 0); + if ( this.gtkVersion == 4 ) + this._accountsWidgetContainer.append(this._accountsWidget, true, true, 0); + else + this._accountsWidgetContainer.add(this._accountsWidget); for ( var _tab of [ 'Settings', 'Columns', 'Colors', 'Filters', 'Replacements' ] ) { @@ -276,6 +296,8 @@ function createPrefWidgets ( noteBook, type, isActive ) visible: true, column_homogeneous: false, }); + if ( this.gtkVersion < 4 ) + grid.margin = 18; noteBook.append_page ( grid, new Gtk.Label ( { label: _(type), } ) ); let y = 0; @@ -340,6 +362,8 @@ function createColumnsPrefTab ( noteBook, type, isActive ) visible: true, column_homogeneous: false, }); + if ( this.gtkVersion < 4 ) + grid.margin = 18; noteBook.append_page ( grid, new Gtk.Label ( { label: _(type), } ) ); let _store = new Gtk.TreeStore(); @@ -354,13 +378,19 @@ function createColumnsPrefTab ( noteBook, type, isActive ) let columnNumbers = new Gtk.TreeViewColumn ( { title: _("Column") } ); let rendererNumbers = new Gtk.CellRendererText ( { editable: true } ); - columnNumbers.append(rendererNumbers, true); + if ( this.gtkVersion == 4 ) + columnNumbers.append(rendererNumbers, true); + else + columnNumbers.pack_start(rendererNumbers, true); columnNumbers.add_attribute(rendererNumbers, 'text', 0); _treeView.append_column(columnNumbers); let _colSize = new Gtk.TreeViewColumn ( { title: _("Size") } ); let _colRenderer = new Gtk.CellRendererText ( { editable: true } ); - _colSize.append(_colRenderer, true); + if ( this.gtkVersion == 4 ) + _colSize.append(_colRenderer, true); + else + _colSize.pack_start(_colRenderer, true); _colSize.add_attribute(_colRenderer, 'text', 1); _treeView.append_column(_colSize); @@ -446,9 +476,18 @@ function addAccountLine ( server_id ) hexpand: true, // margin: 5, halign: Gtk.Align.START } ); - row.set_child ( _label ); - this.accountsChooser.append ( row ); - this.accountsChooser.show_all(); // XXX Does not work in gtk4 ? + if ( this.gtkVersion == 4 ) + { + row.set_child ( _label ); + this.accountsChooser.append ( row ); + } + else + { + _label.margin = 5; + row.add ( _label ); + this.accountsChooser.add ( row ); + this.accountsChooser.show_all(); + } } From d7263f491b6a04b7412d2ee55fd7f0ff6bbbabbe Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 30 May 2022 11:27:27 +0200 Subject: [PATCH 50/55] Implement show all feature --- extension.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/extension.js b/extension.js index 11cab08..233c268 100644 --- a/extension.js +++ b/extension.js @@ -78,8 +78,9 @@ class Indicator extends PanelMenu.Button { this.namesBoxes = { }; this.boxes = { }; - this.sortIcons = { }; + this.showAll = false; + account_settings [ server ] = Preferences.getAccountSettings ( this.server ); this.account_settings = account_settings [ server ]; @@ -165,9 +166,11 @@ class Indicator extends PanelMenu.Button { this._searchField.connect('secondary-icon-clicked', Lang.bind(this, this._onSearchFieldActivate ) ); this._searchField.clutter_text.connect('activate', Lang.bind(this, this._onSearchFieldActivate ) ); this._searchField.clutter_text.connect('text-changed', Lang.bind(this, this._onSearchFieldActivate ) ); - this._buttonMenu.actor.add_child (this._searchField); + this._showAllButton = this._createButton ( 'big', 'view-reveal-symbolic', _('Show all'), this.changeShowAll, true ); + this._buttonMenu.actor.add_child (this._showAllButton ); + this._prefsButton = this._createButton ( 'big', 'preferences-system-symbolic', _('Preferences'), this._onPreferencesActivate ); this._buttonMenu.actor.add_child (this._prefsButton); @@ -224,6 +227,12 @@ class Indicator extends PanelMenu.Button { this.serverLogic.recheckAll ( this._recheckButton ); } + changeShowAll ( ) + { + this.showAll = ! this.showAll; + this.refreshUI ( ); + } + updateStatus ( ) { this.spinChildOf ( this._reloadButton ); @@ -446,7 +455,8 @@ class Indicator extends PanelMenu.Button { entry [ 'status_information' ].toLowerCase().includes ( this._searchString ) ) ) continue; - if ( ( ! _status [ 'WARNING' ] && ! _status [ 'CRITICAL' ] && ! _status [ 'UNKNOWN' ] && entry.status == 'OK' ) || + if ( this.showAll || + ( ! _status [ 'WARNING' ] && ! _status [ 'CRITICAL' ] && ! _status [ 'UNKNOWN' ] && entry.status == 'OK' ) || ( ( _status [ 'WARNING' ] || _status [ 'CRITICAL' ] || _status [ 'UNKNOWN' ] ) && entry.status != 'OK' ) ) { let _style = this.getStyleForRow ( entry.status.toLowerCase(), _row ); @@ -632,7 +642,7 @@ class Indicator extends PanelMenu.Button { this.stopChildSpin ( e.service.button ); } - _createButton ( sizeName, icon, data, callback ) { + _createButton ( sizeName, icon, data, callback, radiobutton = false ) { let size = 24; if ( sizeName == 'small' ) size = 24; @@ -657,6 +667,9 @@ class Indicator extends PanelMenu.Button { }); button.prevIcon = icon; + button.toggle_mode = true; + button.set_checked ( true ); + if ( data instanceof Object ) button.service = data; From d51f470428bb37127b0ba874e382dc51bb8f6d29 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Mon, 30 May 2022 17:16:27 +0200 Subject: [PATCH 51/55] Apply eslinst recommandations --- extension.js | 58 ++++++++++++++++++++++++----------------------- prefs.js | 48 +++++++++++++++++++++++++++------------ servers/icinga.js | 8 ++++--- 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/extension.js b/extension.js index 233c268..3d06e16 100644 --- a/extension.js +++ b/extension.js @@ -24,29 +24,24 @@ const GETTEXT_DOMAIN = 'monito'; -let _httpSession; let _status; -let _ok_text; -const ExtensionUtils = imports.misc.extensionUtils; -const Lang = imports.lang; -const Main = imports.ui.main; -const Mainloop = imports.mainloop; -const Me = ExtensionUtils.getCurrentExtension(); -const PanelMenu = imports.ui.panelMenu; -const PopupMenu = imports.ui.popupMenu; +const ExtensionUtils = imports.misc.extensionUtils; // eslint-disable-line no-undef +const Lang = imports.lang; // eslint-disable-line no-undef +const Main = imports.ui.main; // eslint-disable-line no-undef +const Mainloop = imports.mainloop; // eslint-disable-line no-undef +const Me = ExtensionUtils.getCurrentExtension(); // eslint-disable-line no-undef +const PanelMenu = imports.ui.panelMenu; // eslint-disable-line no-undef +const PopupMenu = imports.ui.popupMenu; // eslint-disable-line no-undef -const Gettext = imports.gettext.domain(GETTEXT_DOMAIN); +const Gettext = imports.gettext.domain(GETTEXT_DOMAIN); // eslint-disable-line no-undef const _ = Gettext.gettext; -const { GObject, St, Clutter, Gio, GLib, Pango } = imports.gi; +const { GObject, St, Clutter, Gio, GLib, Pango } = imports.gi; // eslint-disable-line no-undef const SETTINGS_SCHEMA = "org.gnome.shell.extensions.monito"; -const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito.account"; -const SETTINGS_SCHEMA_ACCOUNT_PATH = "/org/gnome/shell/extensions/monito/account"; const Convenience = Me.imports.convenience; -const GenericServer = Me.imports.servers.genericserver.GenericServer; const Icinga = Me.imports.servers.icinga.Icinga; const Icinga2 = Me.imports.servers.icinga2.Icinga2; const Icinga2API = Me.imports.servers.icinga2api.Icinga2API; @@ -482,11 +477,11 @@ class Indicator extends PanelMenu.Button { this.account_settings.get_string ( 'service-replace' ) ); else if ( _col == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), - this.account_settings.get_string ( 'status-info-replace' ) ) . replace ( new RegExp ( "\n.*" ), "" ); + this.account_settings.get_string ( 'status-info-replace' ) ) . replace ( new RegExp ( "\\n.*" ), "" ); else if ( _col == 'has_been_acknowledged' ) { if ( entry [ _col ] ) - entry [ _col ] = '✔'; // … or ✅🗹 ? + entry [ _col ] = '✔'; else entry [ _col ] = ''; } @@ -550,7 +545,7 @@ class Indicator extends PanelMenu.Button { if (typeof ExtensionUtils.openPrefs === 'function') { ExtensionUtils.openPrefs(); } else { - Util.spawn([ + ExtensionUtils.spawn([ "gnome-shell-extension-prefs", Me.uuid ]); @@ -560,7 +555,6 @@ class Indicator extends PanelMenu.Button { _onSortColumnClick ( button ) { let _sortOrder = Preferences.getSortOrder ( this.server ); - let _columns = Preferences.getColumns ( this.server ); let _indexPlus = _sortOrder.indexOf ( button.column + '+' ); let _indexMinus = _sortOrder.indexOf ( button.column + '-' ); @@ -587,7 +581,7 @@ class Indicator extends PanelMenu.Button { _onExpandLabel ( e ) { - log ( "Monito: Click label " + e.original_text ); + monitoLog ( "Monito: Click label " + e.original_text ); let temp = e.text; e.text = e.original_text; e.original_text = temp; @@ -627,16 +621,16 @@ class Indicator extends PanelMenu.Button { try { proc.wait_finish(result); if ( ! proc.get_successful()) { - log ( 'Monito: the process failed' ); + monitoLog ( 'Monito: the process failed' ); } } catch (e) { - logError(e); + monitoLog(e); } finally { loop.quit(); } }); } catch ( e ) { - log ( 'Monito err: ' + e.message ); + monitoLog ( 'Monito err: ' + e.message ); Main.notifyError ( _('Unable to execute command: %s').format ( e.message ) ); } this.stopChildSpin ( e.service.button ); @@ -650,7 +644,7 @@ class Indicator extends PanelMenu.Button { size = 32; let _text = ''; - if ( ! data instanceof Object ) + if ( ! ( data instanceof Object ) ) _text = data; let button = new St.Button({ @@ -667,8 +661,11 @@ class Indicator extends PanelMenu.Button { }); button.prevIcon = icon; - button.toggle_mode = true; - button.set_checked ( true ); + if ( radiobutton ) + { + button.toggle_mode = true; + button.set_checked ( true ); + } if ( data instanceof Object ) button.service = data; @@ -692,7 +689,7 @@ class Extension { constructor(uuid) { this._uuid = uuid; - settings.connect("changed::servers", Lang.bind ( this, function(stgs, key) { + this._connectId = settings.connect("changed::servers", Lang.bind ( this, function() { this.disable ( ); this.enable ( ); } ) ); @@ -720,19 +717,24 @@ class Extension { this._indicators[i].cancelTimeout(); this._indicators[i].destroy(); } + if ( this._connectId ) + { + settings.disconnect( this._connectId ); + this._connectId = null; + } this._indicators = { }; } } -function init(meta) { +function init(meta) { // eslint-disable-line no-unused-vars return new Extension(meta.uuid); } function monitoLog ( msg ) { - log ( 'Monito: ' + msg ); + log ( 'Monito: ' + msg ); // eslint-disable-line no-undef } diff --git a/prefs.js b/prefs.js index e25e88c..d8d61eb 100644 --- a/prefs.js +++ b/prefs.js @@ -84,7 +84,6 @@ function init() { monitoLog('initializing ${Me.metadata.name} Preferences'); this.settings = ExtensionUtils.getSettings(SETTINGS_SCHEMA); - this.gtkVersion = Gtk.get_major_version(); } @@ -96,6 +95,8 @@ function buildPrefsWidget() { // Copy the same GSettings code from `extension.js` this.settings = ExtensionUtils.getSettings(SETTINGS_SCHEMA); + this._columnsStores = { }; + let mainVbox = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL } ); let mainWidget = new Gtk.Notebook( { } ); @@ -366,12 +367,7 @@ function createColumnsPrefTab ( noteBook, type, isActive ) grid.margin = 18; noteBook.append_page ( grid, new Gtk.Label ( { label: _(type), } ) ); - let _store = new Gtk.TreeStore(); - _store.set_column_types([GObject.TYPE_STRING, GObject.TYPE_INT]); - _store.filter_new(null); - - let _treeView = new Gtk.TreeView ( { model: _store, - headers_visible: true, + let _treeView = new Gtk.TreeView ( { headers_visible: true, reorderable: true, hexpand: true, vexpand: true }); @@ -394,18 +390,25 @@ function createColumnsPrefTab ( noteBook, type, isActive ) _colSize.add_attribute(_colRenderer, 'text', 1); _treeView.append_column(_colSize); - let _item = _store.append(null); - _store.set_value(_item, 0, 'prout' ); - _store.set_value(_item, 1, 2 ); - - _item = _store.append(null); - _store.set_value(_item, 0, 'bar' ); - _store.set_value(_item, 1, 1 ); - // _treeView.connect('row-activated', this._editPath.bind(this)); grid.attach ( _treeView, 0, 1, 1, 1 ); + // Action Bar + let rowsChooserActionBar = new Gtk.ActionBar ( { valign: Gtk.Align.END } ); + + let rowCreateButton = Gtk.Button.new_from_icon_name ( 'list-add', + Gtk.IconSize.BUTTON ); + rowsChooserActionBar.add ( rowCreateButton ); + rowCreateButton.connect ( 'clicked', Lang.bind ( this, this.createColumnRow ) ); + + let rowRemoveButton = Gtk.Button.new_from_icon_name ( 'list-remove', + Gtk.IconSize.BUTTON ); + rowsChooserActionBar.add ( rowRemoveButton ); + rowRemoveButton.connect ( 'clicked', Lang.bind ( this, this.removeColumnRow ) ); + + grid.attach ( columnsChooserActionBar, 0, 2, 1, 1 ); + } @@ -535,6 +538,21 @@ function setColor ( color ) } +function createColumnRow ( ) { + monitoLog ( '> Create column row' ); + + _item = _store.append(null); + _store.set_value(_item, 0, 'bar' ); + _store.set_value(_item, 1, 1 ); + +} + + +function removeColumnRow ( ) { + monitoLog ( '> Remove column row' ); +} + + function monitoLog ( msg ) { log ( 'Monito: ' + msg ); diff --git a/servers/icinga.js b/servers/icinga.js index fe6ed05..18b7fc3 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -48,9 +48,11 @@ class Icinga extends GenericServer { super.prepareHttp ( ); let message = Soup.form_request_new_from_hash ( 'GET', this.urlcgi, { 'jsonoutput': '' } ); - message.request_headers.append ( 'Accept', 'application/json' ); - - this.authenticateAndSend ( message ); + if ( message ) + { + message.request_headers.append ( 'Accept', 'application/json' ); + this.authenticateAndSend ( message ); + } } recheck ( entry ) From 07a62e52808e09608636c77d42bf34fd223941b3 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 9 Sep 2022 11:40:55 +0200 Subject: [PATCH 52/55] Horrible fix to prevent variable from being modified later --- servers/icinga.js | 2 +- servers/icinga2.js | 3 +-- servers/icinga2api.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/servers/icinga.js b/servers/icinga.js index 18b7fc3..e6c163d 100644 --- a/servers/icinga.js +++ b/servers/icinga.js @@ -110,7 +110,7 @@ class Icinga extends GenericServer { getUrlForService ( service ) { - return '%s/?type=2&host=%s&service=%s'.format ( this._settings.get_string ( 'urlcgi' ).replace ( /status.cgi/, 'extinfo.cgi' ), + return '%s?type=2&host=%s&service=%s'.format ( this._settings.get_string ( 'urlcgi' ).replace ( /status.cgi/, 'extinfo.cgi' ), encodeURI ( service.real_host_name ), encodeURI ( service.service_display_name ) ); } diff --git a/servers/icinga2.js b/servers/icinga2.js index 1e924ba..c47f7fb 100644 --- a/servers/icinga2.js +++ b/servers/icinga2.js @@ -82,9 +82,8 @@ class Icinga2 extends GenericServer { getUrlForService ( service ) { - log ( service ); return '%s/monitoring/service/show?host=%s&service=%s'.format ( this._settings.get_string ( 'url' ), - service.host_name, + service.real_host_name, service.service_display_name ); } diff --git a/servers/icinga2api.js b/servers/icinga2api.js index ce9bb79..4be811b 100644 --- a/servers/icinga2api.js +++ b/servers/icinga2api.js @@ -102,7 +102,7 @@ class Icinga2API extends GenericServer { // log ( JSON.stringify(entry) ); this.status.service_status.push ( { status: _statuses[entry.attrs.state], - real_host_name: entry.attrs.host_name, + real_host_name: entry.attrs.host_name + '', host_name: entry.attrs.host_name, service_display_name: entry.attrs.display_name, has_been_acknowledged: parseInt(entry.attrs.acknowledgement), From c8d0b98348874f24fdd6969266645cff4c668fd6 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 9 Sep 2022 11:41:32 +0200 Subject: [PATCH 53/55] Start to implement column configuration --- prefs.js | 54 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/prefs.js b/prefs.js index d8d61eb..d03c644 100644 --- a/prefs.js +++ b/prefs.js @@ -169,12 +169,10 @@ function buildPrefsWidget() { let accountsChooserContainer = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL, homogeneous: false, } ); - monitoLog ( 'Box ' + this._accountsWidgetContainer ); - monitoLog ( 'Func ' + this._accountsWidgetContainer.pack_start ); if ( this.gtkVersion == 4 ) this._accountsWidgetContainer.append(accountsChooserContainer, true, true, 0); else - this._accountsWidgetContainer.add(accountsChooserContainer, true, true, 0); + this._accountsWidgetContainer.add(accountsChooserContainer); this.accountsChooser = new Gtk.ListBox ( { valign: Gtk.Align.FILL, hexpand: true, @@ -182,7 +180,7 @@ function buildPrefsWidget() { if ( this.gtkVersion == 4 ) accountsChooserContainer.append(this.accountsChooser, true, true, 0); else - accountsChooserContainer.add(this.accountsChooser, true, true, 0); + accountsChooserContainer.add(this.accountsChooser); // Account list for ( var server_id of this.getServersList ( ) ) @@ -194,7 +192,7 @@ function buildPrefsWidget() { if ( this.gtkVersion == 4 ) accountsChooserContainer.append(accountsChooserActionBar, true, true, 0); else - accountsChooserContainer.add(accountsChooserActionBar, true, true, 0); + accountsChooserContainer.add(accountsChooserActionBar); let accountCreateButton = Gtk.Button.new_from_icon_name ( 'list-add', Gtk.IconSize.BUTTON ); @@ -367,19 +365,21 @@ function createColumnsPrefTab ( noteBook, type, isActive ) grid.margin = 18; noteBook.append_page ( grid, new Gtk.Label ( { label: _(type), } ) ); - let _treeView = new Gtk.TreeView ( { headers_visible: true, + this.treeView = new Gtk.TreeView ( { headers_visible: true, reorderable: true, hexpand: true, vexpand: true }); let columnNumbers = new Gtk.TreeViewColumn ( { title: _("Column") } ); - let rendererNumbers = new Gtk.CellRendererText ( { editable: true } ); + let rendererNumbers = new Gtk.CellRendererCombo ( { editable: true } ); + rendererNumbers.col = 0; if ( this.gtkVersion == 4 ) columnNumbers.append(rendererNumbers, true); else columnNumbers.pack_start(rendererNumbers, true); columnNumbers.add_attribute(rendererNumbers, 'text', 0); - _treeView.append_column(columnNumbers); + this.treeView.append_column(columnNumbers); + rendererNumbers.connect('edited', Lang.bind ( this, this.editColumn ) ); let _colSize = new Gtk.TreeViewColumn ( { title: _("Size") } ); let _colRenderer = new Gtk.CellRendererText ( { editable: true } ); @@ -388,11 +388,13 @@ function createColumnsPrefTab ( noteBook, type, isActive ) else _colSize.pack_start(_colRenderer, true); _colSize.add_attribute(_colRenderer, 'text', 1); - _treeView.append_column(_colSize); + this.treeView.append_column(_colSize); + _colRenderer.col = 1; + _colRenderer.connect('edited', Lang.bind ( this, this.editColumn ) ); // _treeView.connect('row-activated', this._editPath.bind(this)); - grid.attach ( _treeView, 0, 1, 1, 1 ); + grid.attach ( this.treeView, 0, 1, 1, 1 ); // Action Bar let rowsChooserActionBar = new Gtk.ActionBar ( { valign: Gtk.Align.END } ); @@ -407,7 +409,7 @@ function createColumnsPrefTab ( noteBook, type, isActive ) rowsChooserActionBar.add ( rowRemoveButton ); rowRemoveButton.connect ( 'clicked', Lang.bind ( this, this.removeColumnRow ) ); - grid.attach ( columnsChooserActionBar, 0, 2, 1, 1 ); + grid.attach ( rowsChooserActionBar, 0, 2, 1, 1 ); } @@ -418,6 +420,11 @@ function activateAccountRow ( ) { this._accountsWidget.destroy ( ); this.createAccountWidgets ( true ); + this.store = new Gtk.ListStore(); + this.store.set_column_types([GObject.TYPE_STRING, GObject.TYPE_INT]); + this.store.filter_new(null); + this.treeView.model = this.store; + let _row = this.accountsChooser.get_selected_row(); monitoLog('Active:' + _row.server); let _account_settings = getAccountSettings ( _row.server ); @@ -541,15 +548,30 @@ function setColor ( color ) function createColumnRow ( ) { monitoLog ( '> Create column row' ); - _item = _store.append(null); - _store.set_value(_item, 0, 'bar' ); - _store.set_value(_item, 1, 1 ); - + let _item = this.store.append(null); + this.store.set_value(_item, 0, 'bar' ); + this.store.set_value(_item, 1, 1 ); } -function removeColumnRow ( ) { +function removeColumnRow ( widget ) { monitoLog ( '> Remove column row' ); + let [any, model, _iter] = this.treeView.get_selection().get_selected ( ); + this.store.remove ( _iter ); +} + + +function editColumn ( widget, path, text ) +{ + monitoLog ( '> Edit column row ' + widget.col + ', ' + path + ', ' + text ) + let _iter = this.store.get_iter ( Gtk.TreePath.new_from_string ( path ) ); + monitoLog ( '> Iter ' + _iter[1] ); + if ( widget.col == 1 ) + this.store.set_value(_iter[1], widget.col, parseInt(text) ); + else + this.store.set_value(_iter[1], widget.col, text ); + + return true; } From ededa75d940b91249a6c265c0e5e911bca1e65d9 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 9 Sep 2022 18:11:22 +0200 Subject: [PATCH 54/55] Import of that file --- package.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..8711e5f --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "monito", + "version": "1.0.0", + "description": "Monito extension for gnome-shell", + "main": "extension.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "ssh://gitea@forge.chapril.org:222/Monito/monito.git" + }, + "author": "Benjamin Drieu", + "license": "GPL-2.0-or-later" +} From 00cdfc4a815fa0ebc6201ced4ee3c91ec9ec6aa0 Mon Sep 17 00:00:00 2001 From: Benjamin Drieu Date: Fri, 9 Sep 2022 19:22:46 +0200 Subject: [PATCH 55/55] Actually let user change columns --- extension.js | 48 ++++---- prefs.js | 109 +++++++++++++++--- ...ll.extensions.monito@drieu.org.gschema.xml | 6 +- 3 files changed, 118 insertions(+), 45 deletions(-) diff --git a/extension.js b/extension.js index 3d06e16..4b5cdf4 100644 --- a/extension.js +++ b/extension.js @@ -276,7 +276,7 @@ class Indicator extends PanelMenu.Button { }); } - createHeaderBin ( colName ) { + createHeaderBin ( colName, colSize = 50 ) { let col = column_definitions [ colName ]; let _box = new St.BoxLayout ( { vertical: false, @@ -305,7 +305,7 @@ class Indicator extends PanelMenu.Button { let _button = new St.Button ( { x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.CENTER, - width: col.width, + width: colSize, reactive: true, can_focus: true, track_hover: true, @@ -319,7 +319,7 @@ class Indicator extends PanelMenu.Button { let _bin = new St.Bin({ style_class: 'monito-service', - width: col.width, + width: colSize, x_expand: col.expand, child: _button, }); @@ -327,7 +327,7 @@ class Indicator extends PanelMenu.Button { return _bin; } - createBin ( status, text, col ) { + createBin ( status, text, colSize = 50, col ) { let _child; if ( text === undefined ) @@ -339,7 +339,7 @@ class Indicator extends PanelMenu.Button { reactive: true, can_focus: true, track_hover: true, - width: col.width, + width: colSize, text: text.toString().replace(/\n.*/s, ''), x_align: ( col.align ? col.align : Clutter.ActorAlign.START ), style: ( col.style ? col.style : '' ) } ); @@ -351,7 +351,7 @@ class Indicator extends PanelMenu.Button { { _child = new St.BoxLayout ( { x_expand: true, vertical: false, - width: col.width, } ); + width: colSize, } ); if ( this.serverLogic.canRecheck ) { @@ -363,7 +363,7 @@ class Indicator extends PanelMenu.Button { let _bin = new St.Bin({ track_hover: true, - width: col.width, + width: colSize, x_expand: col.expand, child: _child, }); @@ -421,10 +421,8 @@ class Indicator extends PanelMenu.Button { }); this._box.add_child(headerBox); - let _columns = Preferences.getColumns ( this.server ); - for ( let _col of _columns ) - headerBox.add_child ( this.createHeaderBin ( _col ) ); - headerBox.add_child ( this.createHeaderBin ( 'actions' ) ); + for ( let _col of Preferences.getColumns ( this.server ) ) + headerBox.add_child ( this.createHeaderBin ( _col.name, _col.size ) ); let scrollBox = new St.ScrollView ( { hscrollbar_policy: St.PolicyType.NEVER, enable_mouse_scrolling: true, } ); @@ -465,30 +463,28 @@ class Indicator extends PanelMenu.Button { }); tableBox.add_child(infoBox); - let _columns = Preferences.getColumns ( this.server ); - for ( let _col of _columns ) + for ( let _col of Preferences.getColumns ( this.server ) ) { - entry [ 'real_' + _col ] = entry [ _col ]; - if ( _col == 'host_name' && this.account_settings.get_string ( 'host-match' ) ) - entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'host-match' ), 'i' ), + entry [ 'real_' + _col.name ] = entry [ _col.name ]; + if ( _col.name == 'host_name' && this.account_settings.get_string ( 'host-match' ) ) + entry [ _col.name ] = entry [ _col.name ] . replace ( new RegExp ( this.account_settings.get_string ( 'host-match' ), 'i' ), this.account_settings.get_string ( 'host-replace' ) ); - else if ( _col == 'service_display_name' && this.account_settings.get_string ( 'service-match' ) ) - entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'service-match' ), 'i' ), + else if ( _col.name == 'service_display_name' && this.account_settings.get_string ( 'service-match' ) ) + entry [ _col.name ] = entry [ _col.name ] . replace ( new RegExp ( this.account_settings.get_string ( 'service-match' ), 'i' ), this.account_settings.get_string ( 'service-replace' ) ); - else if ( _col == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) - entry [ _col ] = entry [ _col ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), + else if ( _col.name == 'status_information' && this.account_settings.get_string ( 'status-info-match' ) ) + entry [ _col.name ] = entry [ _col.name ] . replace ( new RegExp ( this.account_settings.get_string ( 'status-info-match' ), 'i' ), this.account_settings.get_string ( 'status-info-replace' ) ) . replace ( new RegExp ( "\\n.*" ), "" ); - else if ( _col == 'has_been_acknowledged' ) + else if ( _col.name == 'has_been_acknowledged' ) { - if ( entry [ _col ] ) - entry [ _col ] = '✔'; + if ( entry [ _col.name ] ) + entry [ _col.name ] = '✔'; else - entry [ _col ] = ''; + entry [ _col.name ] = ''; } - infoBox.add_child ( this.createBin ( entry.status, entry [ _col ], column_definitions [ _col ] ) ); + infoBox.add_child ( this.createBin ( entry.status, entry [ _col.name ], _col.size, column_definitions [ _col.name ] ) ); } - infoBox.add_child ( this.createBin ( entry.status, entry, column_definitions [ 'actions' ] ) ); _row ++; } diff --git a/prefs.js b/prefs.js index d03c644..080a29d 100644 --- a/prefs.js +++ b/prefs.js @@ -44,6 +44,18 @@ const SETTINGS_SCHEMA_ACCOUNT = "org.gnome.shell.extensions.monito.account"; const SETTINGS_SCHEMA_ACCOUNT_PATH = "/org/gnome/shell/extensions/monito/account"; +const column_definitions = { + status: { label: _('Status'), width: 50, expand: false, }, + host_name: { label: _('Host name'), width: 300, expand: false, }, + service_display_name: { label: _('Service'), width: 300, expand: false, }, + has_been_acknowledged: { label: _('Ack'), width: 50, expand: false }, + last_check: { label: _('Last check'), width: 200, expand: false, type: 'date' }, + attempts: { label: _('Attempts'), width: 50, expand: false, }, + status_information: { label: _('Information'), width: 600, expand: false, }, + actions: { label: 'Actions', width: 50, expand: false, special: 'actions' }, +}; + + const prefs = [ { type: Gtk.Switch, category: 'Settings', label: _('Active (restart for effect)'), key: 'active', align: Gtk.Align.START }, { type: Gtk.Entry, category: 'Settings', label: _('Name'), key: 'name' }, @@ -246,7 +258,11 @@ function setSortOrder ( server, sort_order ) function getColumns ( server ) { - return this.getAccountSettings ( server ) . get_strv ( 'columns' ); + let _columns = this.getAccountSettings ( server ) . get_strv ( 'columns' ); + let _columnsSizes = this.getAccountSettings ( server ) . get_strv ( 'columns-size' ); + const zip = (a1, a2) => a1.map((x, i) => { return { name: x, size: a2[i] } } ); + let _result = zip ( _columns, _columnsSizes ); + return _result; } @@ -371,18 +387,21 @@ function createColumnsPrefTab ( noteBook, type, isActive ) vexpand: true }); let columnNumbers = new Gtk.TreeViewColumn ( { title: _("Column") } ); - let rendererNumbers = new Gtk.CellRendererCombo ( { editable: true } ); - rendererNumbers.col = 0; + this.ColumnNameRenderer = new Gtk.CellRendererCombo ( { editable: true, + has_entry: false, + text_column: 0 } ); + this.ColumnNameRenderer.col = 0; if ( this.gtkVersion == 4 ) - columnNumbers.append(rendererNumbers, true); + columnNumbers.append(this.ColumnNameRenderer, true); else - columnNumbers.pack_start(rendererNumbers, true); - columnNumbers.add_attribute(rendererNumbers, 'text', 0); + columnNumbers.pack_start(this.ColumnNameRenderer, true); + columnNumbers.add_attribute(this.ColumnNameRenderer, 'text', 0); this.treeView.append_column(columnNumbers); - rendererNumbers.connect('edited', Lang.bind ( this, this.editColumn ) ); + this.ColumnNameRenderer.connect('edited', Lang.bind ( this, this.editColumn ) ); let _colSize = new Gtk.TreeViewColumn ( { title: _("Size") } ); - let _colRenderer = new Gtk.CellRendererText ( { editable: true } ); + let _colRenderer = new Gtk.CellRendererSpin ( { adjustment: new Gtk.Adjustment ( { lower: 0, upper: 999, step_increment: 1, page_increment: 10 } ), + editable: true } ); if ( this.gtkVersion == 4 ) _colSize.append(_colRenderer, true); else @@ -426,12 +445,30 @@ function activateAccountRow ( ) { this.treeView.model = this.store; let _row = this.accountsChooser.get_selected_row(); - monitoLog('Active:' + _row.server); let _account_settings = getAccountSettings ( _row.server ); + this.columnsModel = new Gtk.ListStore ( ); + this.columnsModel.set_column_types([GObject.TYPE_STRING,GObject.TYPE_STRING]); + for ( let [ _colName, _colDef ] of Object.entries(column_definitions) ) + { + let _iter = this.columnsModel.append(); + this.columnsModel.set_value(_iter, 0, _colName ); + this.columnsModel.set_value(_iter, 1, _colDef.label ); + } + this.ColumnNameRenderer.model = this.columnsModel; + if ( ! _account_settings ) return; + let _columns = _account_settings . get_strv ( 'columns' ); + let _columnsSizes = _account_settings . get_strv ( 'columns-size' ); + for ( var i in _columns ) + { + let _iter = this.store.append(); + this.store.set_value(_iter, 0, _columns [ i ] ); + this.store.set_value(_iter, 1, parseInt(_columnsSizes [ i ]) ); + } + for ( var prefEntry of prefs ) { if ( prefEntry.type == Gtk.Entry ) @@ -464,8 +501,6 @@ function activateAccountRow ( ) { { this.prefWidgets[prefEntry.key].set_active(_account_settings.get_enum('type')); this.prefWidgets[prefEntry.key].connect('changed', Lang.bind(this, function (e) { - monitoLog ( e ) ; - monitoLog('Active:' + this.prefWidgets['type'].get_active()); let _account_settings = getAccountSettings ( _row.server ); _account_settings.set_enum('type', this.prefWidgets['type'].get_active()); })); @@ -546,30 +581,68 @@ function setColor ( color ) function createColumnRow ( ) { - monitoLog ( '> Create column row' ); + let _item = this.store.append(); + this.store.set_value(_item, 0, 'status' ); + this.store.set_value(_item, 1, 300 ); - let _item = this.store.append(null); - this.store.set_value(_item, 0, 'bar' ); - this.store.set_value(_item, 1, 1 ); + let _row = this.accountsChooser.get_selected_row(); + let _account_settings = getAccountSettings ( _row.server ); + let prefKey; + + let _defs = _account_settings . get_strv ( 'columns' ); + _defs.push ( 'status' ); + _account_settings . set_strv ( 'columns', _defs ); + + _defs = _account_settings . get_strv ( 'columns-size' ); + _defs.push ( '300' ); + _account_settings . set_strv ( 'columns-size', _defs ); } function removeColumnRow ( widget ) { - monitoLog ( '> Remove column row' ); let [any, model, _iter] = this.treeView.get_selection().get_selected ( ); + + let _row = this.accountsChooser.get_selected_row(); + let _account_settings = getAccountSettings ( _row.server ); + let prefKey; + + for ( var foo of this.treeView.get_selection().get_selected_rows()[0] ) + { + + let _defs = _account_settings . get_strv ( 'columns' ); + _defs.splice ( foo.get_indices ( ), 1 ); + _account_settings . set_strv ( 'columns', _defs ); + + _defs = _account_settings . get_strv ( 'columns-size' ); + _defs.splice ( foo.get_indices ( ), 1 ); + _account_settings . set_strv ( 'columns-size', _defs ); + } + this.store.remove ( _iter ); } function editColumn ( widget, path, text ) { - monitoLog ( '> Edit column row ' + widget.col + ', ' + path + ', ' + text ) + let _row = this.accountsChooser.get_selected_row(); + let _account_settings = getAccountSettings ( _row.server ); + let prefKey; + let _iter = this.store.get_iter ( Gtk.TreePath.new_from_string ( path ) ); - monitoLog ( '> Iter ' + _iter[1] ); if ( widget.col == 1 ) + { this.store.set_value(_iter[1], widget.col, parseInt(text) ); + prefKey = 'columns-size'; + } else + { this.store.set_value(_iter[1], widget.col, text ); + prefKey = 'columns'; + } + + let _defs = _account_settings . get_strv ( prefKey ); + _defs [ parseInt(path) ] = text; + _account_settings . set_strv ( prefKey, _defs ); return true; } diff --git a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml index 0f0d9e0..24ef178 100644 --- a/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml +++ b/schemas/org.gnome.shell.extensions.monito@drieu.org.gschema.xml @@ -134,7 +134,11 @@ - ['status','host_name','service_display_name','has_been_acknowledged','last_check','attempts','status_information'] + ['status','host_name','service_display_name','has_been_acknowledged','last_check','attempts','status_information','actions'] + + + + [ '50', '300', '300', '50', '200', '50', '600', '50' ]