From c90e154645a6409b73d33b0671e4b36160775c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Mon, 14 Oct 2013 13:51:09 +0200 Subject: [PATCH 01/12] Implement hosts view refs #4824 Conflicts: modules/monitoring/application/views/scripts/list/hosts.phtml modules/monitoring/library/Monitoring/Object/Host.php public/css/icons.css public/css/main.css --- public/img/images/acknowledgement.png | Bin 0 -> 501 bytes public/img/images/comment.png | Bin 0 -> 491 bytes public/img/images/create.png | Bin 0 -> 475 bytes public/img/images/dashboard.png | Bin 0 -> 415 bytes public/img/images/disabled.png | Bin 0 -> 535 bytes public/img/images/edit.png | Bin 0 -> 486 bytes public/img/images/error.png | Bin 0 -> 532 bytes public/img/images/flapping.png | Bin 0 -> 621 bytes public/img/images/in_downtime.png | Bin 0 -> 490 bytes public/img/images/remove.png | Bin 0 -> 661 bytes public/img/images/save.png | Bin 0 -> 506 bytes public/img/images/service.png | Bin 0 -> 496 bytes public/img/images/submit.png | Bin 0 -> 418 bytes public/img/images/unhandled.png | Bin 0 -> 553 bytes public/img/images/user.png | Bin 0 -> 487 bytes 15 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/img/images/acknowledgement.png create mode 100644 public/img/images/comment.png create mode 100644 public/img/images/create.png create mode 100644 public/img/images/dashboard.png create mode 100644 public/img/images/disabled.png create mode 100644 public/img/images/edit.png create mode 100644 public/img/images/error.png create mode 100644 public/img/images/flapping.png create mode 100644 public/img/images/in_downtime.png create mode 100644 public/img/images/remove.png create mode 100644 public/img/images/save.png create mode 100644 public/img/images/service.png create mode 100644 public/img/images/submit.png create mode 100644 public/img/images/unhandled.png create mode 100644 public/img/images/user.png diff --git a/public/img/images/acknowledgement.png b/public/img/images/acknowledgement.png new file mode 100644 index 0000000000000000000000000000000000000000..eb03d2db9fd1aa0a8b86c45d4fd31cbbda8e12fe GIT binary patch literal 501 zcmVT(!FYwK@`UE-y1AVA>fLztJuQ?pK#uz0aPA zel?;WNA$Oc_b8(O7SW$-t?628N{My6!v}0+)@kQ8e8Pjwx>HI##}%BY3J zuawxuMO>JNagYJLMK={0gm!TW=Q8V$ML%?~NTozK*q-1Pp5Q9hGwXN}Jk0=P)`sgC r<15y1_FwQH4rXk_Hl8h-dA@!DHO8Q#1R#?V00000NkvXXu0mjfW){$H literal 0 HcmV?d00001 diff --git a/public/img/images/comment.png b/public/img/images/comment.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d88a0ace463d9d9df4fc6c1ecf8ae4fba7fd05 GIT binary patch literal 491 zcmVL(oc)dQ4|O8&u^L%i`hw7iF@u=DT_gonzEf_H(tY#vQvuPY@}x46)3V$ zY$*#HCU(wkWNBk#V?nwWeovm6H1ja0?&93;_n!0pGxXl=rRMg8#{6_sYaGBb&ZA-h z1HK~h5>GPQw+UvLP^dLlv5L=ljaT?IzB`7qID?~D%WU^R?_ES$k0=>Y&h_5ybRx>d zh_V?`)_d=Uh;nCK`($SOJ`3+!V?Q?VSVZX&j`*E{E4YeF{sieGUf^M7`!d07N#fWa!c#oMQfAxSiPH8D3LfEZX8W+494nJ za0`JjZQG2Dvly;50{sg z1Q>i!TGMp)&q-e26y7}??@wk=a7|=oK6}Q+W%H}q0wUq9lBXA}zP{{j<42d$Rep1S z?c44*wb)F=uuWKI+82MXuMY$69_l%*ob%9}Z-Z=d#LV4}H|IZ)sXtv+xT3&hF=I%? z>ow{7Ohn|1-$x@M8;p02{4#7ns|f@~#4Z$-~2<2%0NQ1MCq!v5{=lnvW% zYqj+D)W-b&6ty(OzHI3+iEGtnWrf$AVswh4Pdex9dj3Cqm4KPwhQ&WuZA<9kmYMoT zqPikqG|S>{h-G@JN0h1L>52pECtbgx;}candCIHnf=fd7fBU2NVCmr}tp1u;|Nq*T zcJ|4nJ za0`Jj-fdK@bMu=N>M$1}yvtDJ*uj6G8ugaJdGo#0XZ3U}qgGg=lFkw6ON0 zQqUh@lvFmNf|h%FZ!jQ6u(EJ1a;}^NP42)fyR+~6?(8r#sI}@_&M}7@xQzKO`~^>O zx0H6Pk00~}a*j*5g$=CX6B?YrxD&ixO8e|Dsn)92Dk9y9NE;DpG>z@{h;%k0eT_&f z)7bRlVno`ENJsuh?ujf!q^*dwRBJVeNC)u_*Gg&Mn!$38F&^S{(-mwoD;Y{@*D%5I z5La=(r+#1`@~>|D3fCJP#pjMZ!neJ=!@vg|Yr4blS<~;?aJo!z60b{X-{c&JX8gh_ zOon)jMXc@u>}O*Q_lUGGLn9rJNIxUeg<7koOZx>L z;A75lZjYDG<2~+`(!S*1!?c1;tasqXcELtn+BZvSpHDeE1Uiv9EaQru4nJ za0`JjReY2pud27?%7X5YCtUC4974EPs zu!zg|S8$H2nDpLz(Y35MKAF4bUHal?Kl@5lucYPj$ra7P71Dw067R5CE*GsiB>d)q z?7j!TK5^{n>aUOre7EI?)47-CcKWi-*QF;k}f8%dHH}Ct0l`cDPo__yW>zvz^ z#XEJM%(?b4k*}`fw1s$J--G#LVh`AP=l5&0$L=#Zkj-m9yLtPDo5wfU9H{2aKd)JB zyr@Px%x2}*Qvfc literal 0 HcmV?d00001 diff --git a/public/img/images/error.png b/public/img/images/error.png new file mode 100644 index 0000000000000000000000000000000000000000..62458999f89ed83086512a3a09d7dfbc92b30de8 GIT binary patch literal 532 zcmV+v0_**WP)L(lKk)K@bJ--#@tnK}akttOUW^=`F-Yw6L;J3me5+1wl|zR1hMlRs0xQ z*?4FmjfIv0tAOrWDWngH7K*SIw}DG?ITQyL-n@M?`(~N0*6M#^X4+*$dK!@yr)j#q z97LoOc#94faxU*D>3UO|KgUf}EaLbi(ZmrV(penB3%ta^h;;6+8{iop=Un7m;7Q|l zN3C_+NojC7rNL^$ODPSO8vc~h;L1+@HU|-D77KXTs8cxI@O>;a&7LD%#YWC$7?Jwe zz-C11=UiUn2d+SJ1`TGj^HA0 zjDJqf%RpH`;whEBw9X`^R|rX|&$`9uD9b&iJ>e`1%K( WnT^lgNzbML00005)6a{~K^O<{*Y|71+NF@#^1J=cC|hz;e(aW%)RqeeHf`;;YX5*eh@2d@ zjfexq!9j9yuu@9$qZKzv4l~K7D3{%XA}1e*osYgbt*54D-t){n@AvzeQB{@2ju2Ws zA+-A2v8Mlvi+CDi7^;De7{gxlqJf7g<@tIu#?U}NmZ6C~n7~wwVP#7BMh9NuIXWg+l3dU!rD@G8&0K^pOl;#?8B{;@>oqBq?CVP z5UVhPIb5y}ml!V>j@7`wZCqWZjW`?+77f!=)p7mE-dEDfr*szos#ngJMj{O zDdnmWLhA%};7Fkza2OY`5ncFD*!JNlrm!3*o48wyxrgnTOeufF8EnFBe8yJ{rIcq= z%Ev8qC^zvFYl=Z{Q_44ssk4~GISiCBUZfnr7F=zF(CWqJLUAUg{Hxuh7(*|Xmi_y9 zWZ|8Rrj#En`X`p(cs+#HTnMd0HTJg)i-{ORCwfxKpK9z6dK25uY2{{u00000NkvXX Hu0mjfTF@0O literal 0 HcmV?d00001 diff --git a/public/img/images/in_downtime.png b/public/img/images/in_downtime.png new file mode 100644 index 0000000000000000000000000000000000000000..d609df62e332dc928542da1f18ad85d60e747e6b GIT binary patch literal 490 zcmVdHQ5XgA-Jjbt;>a7;>|ASUCcMm!- zhG7J(VgEL9#%HX>7*0@EAxxa1!k3il_xCUmWB7nYTvj1Wbg_;f7{q$gGR5*=&(ocxjRTKsA&u@}4#yFopbL!=Z^ZmS4zBq z1^k*>k9y82C9YsSwq@3TM-kmP&SDL=;#_7u`9I`oti)my*}npp@ONgNEG6E+@1?|e zX8m+PgjuZ0tnW5#9>0v?XKXAbHf7dpcpC>wiO&YbXIkYY@-A$_7o+CEF&xGA%zC4g z*o5EkHQvPP%sSP`@8BZ#W!C92$gC%D2A4~T*E8#N?7(*18jvSi$A>cOH0b;O@r~#{ zkLa#PbQ^|vdz*;p?niVV4e?Rm_fI>0vQ^)kS(mVvaTOnD)^CRY$CgPe@Yhq=1N_-W zdg46MK`AkV6?iYRJ}4z#!9SVxekt*4Gj=Ys9(hKfl$ga!nRV~L?eB1-3EpZV3r*zk z;}P11jbn1)_Cdbntr7|^&=+@#qUi4_|j%U_ymq|x!dpG_- vhYy!lc^p6CtIT?OIr2c{0`_2YX1((tPzUQN{0FNI00000NkvXXu0mjf!6zy~ literal 0 HcmV?d00001 diff --git a/public/img/images/save.png b/public/img/images/save.png new file mode 100644 index 0000000000000000000000000000000000000000..2db271920d585eaeffc667dd78df6160689859ac GIT binary patch literal 506 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ za0`JjLLym#5l7t?1mG){EDeTV4W0?? z5oX0-onm&F7MFW2Uv-ADS<@lo*}8^Vt6C1Ut_tBQ+<06%j`>@Iw7x6Dl+dL~&-l*V z*|>^_Ii2;EDU-~soxJx$rq(80*__1?*^r(P8@es{pVEfcd&NI9T(vk_k=2^@=RxIL z24iuiXvqgYTNy)AIbNK4nLl&q@|cEIGK`xKWD8vkX8E$})Yh5NXT1%0br|?9In^3I z`)QjrE}QxL#H_HX-*-HBe;LcX!20>?m;X+f3%ydm@PZ-ZL-p4D4||L)3Hk(K@i9BIqkiviv(z{z&`-*5`3JW11B>#wz&dL<_f`5S|MXIE- z2m~<%6i2CGBw=?tm?(ED?R+gn;c+K z>|`NKmsr8~I{xQr##rU}mQsH3fZN3w{;0}^&OOQNF^1KayVC^RCGOx2-o_Xv z+wmIkw;@=KF}!c&yuuG0VXfi6ya0jz|$JAh&@OtFJcT+RmnX(ODXTRdIIzKT=(E2rMyTf zFDl46wy=zM&89W-E};GgqwVfuvwH8R|HS&58c?*ODdi`4d5gHOsR8pbhDQe9g%Wez mEYpGkK2^*9VK*Cgov#4So@B%YS!sX(00004nJ za0`JjYhUAuyT`&ebV!&M@r;QSn9lm$I4lM!VYrEeQen+;k@1xsKl`C9e;{s_zwS- zR^5hEZXEIRy1Im8q5UDTd(xNmSz>z%=LCHXh?YtcZB}ZjJEoWyEg~YZJj6G>*|5#~ zjrpeOSu1lh&Wb92@y-fdK@bMu=N_(Bf_8p{6c#(D6G8ug5M#hfP_a@1{)GraglK7MYdI2#~Zs7s8u!*l|a1M**z=t_^FaL@9zSsA?i1a8TZAGMoS!`$#=}JWU5s}tr zu^DkAB5g;cQv<<5M7lhP%}1nZM7rMhy-`Fuj!(FobN8|O&Jp}B+oL&myA`*HC%D*@ zf=z1itUmZvY*Z6Dcbk}CrNJ`Z+8=$f{qPRU4Nl|R0ZE793(kzoX77O8k5&E<i*gp*irN^h^? zP20wNmA8k_#r7)kS-i&moV%U6!`4bSUOl9huSKMvbt~^TOl#Q2hjM#U3bs(5y_$3P rYNi<-FwQHuL(l3ZrQ4q)R&s#(^Y*?OUx0vpXY*eG&s>z~)3kJ7{Agf6%f`WfAX%aE0 z#jYT1Sy&K@f?=*%i&fBe4ezczpO<|u4xI9xncvJg=QrxQ&i}-|A($B}xQ3%WdWYLp z?YG(5g@NYfzF`s9aRZ03of%92vcWn&RJChUY%?>qu!eiPZh%AhIKV$)dF~bP69)(Q zLZ6!}@Ezj;KJIgK1>WFzX8g`&#(o^b+qnW&?G}FE{1m=~FIDaP+4?~r;1OII-L0ffzISh3B}x d$L5`Q@e2jFb Date: Wed, 9 Oct 2013 16:38:46 +0200 Subject: [PATCH 02/12] Design: Host and service list Fix: Command controller and downtimes refs #4824 --- public/img/icons/acknowledgement.png | Bin 477 -> 501 bytes public/img/icons/error.png | Bin 530 -> 532 bytes public/img/icons/flapping.png | Bin 629 -> 621 bytes public/img/images/acknowledgement.png | Bin 501 -> 0 bytes public/img/images/comment.png | Bin 491 -> 0 bytes public/img/images/create.png | Bin 475 -> 0 bytes public/img/images/dashboard.png | Bin 415 -> 0 bytes public/img/images/disabled.png | Bin 535 -> 0 bytes public/img/images/edit.png | Bin 486 -> 0 bytes public/img/images/error.png | Bin 532 -> 0 bytes public/img/images/flapping.png | Bin 621 -> 0 bytes public/img/images/in_downtime.png | Bin 490 -> 0 bytes public/img/images/remove.png | Bin 661 -> 0 bytes public/img/images/save.png | Bin 506 -> 0 bytes public/img/images/service.png | Bin 496 -> 0 bytes public/img/images/submit.png | Bin 418 -> 0 bytes public/img/images/unhandled.png | Bin 553 -> 0 bytes public/img/images/user.png | Bin 487 -> 0 bytes 18 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/img/images/acknowledgement.png delete mode 100644 public/img/images/comment.png delete mode 100644 public/img/images/create.png delete mode 100644 public/img/images/dashboard.png delete mode 100644 public/img/images/disabled.png delete mode 100644 public/img/images/edit.png delete mode 100644 public/img/images/error.png delete mode 100644 public/img/images/flapping.png delete mode 100644 public/img/images/in_downtime.png delete mode 100644 public/img/images/remove.png delete mode 100644 public/img/images/save.png delete mode 100644 public/img/images/service.png delete mode 100644 public/img/images/submit.png delete mode 100644 public/img/images/unhandled.png delete mode 100644 public/img/images/user.png diff --git a/public/img/icons/acknowledgement.png b/public/img/icons/acknowledgement.png index ff7b92cf88d71b59a67538722703e6d0a320d3a7..eb03d2db9fd1aa0a8b86c45d4fd31cbbda8e12fe 100644 GIT binary patch delta 361 zcmV-v0ha#V1N8%tm48b~L_t(IjjhtXYLr0~#_``9EKMQg4&*%pNg+to&JPgnj9~6S z5K`C)#zwpWv5l*y(Z&l1XrTy-O*$*j6yAYuZ?R+HyJ1~cNr(pqW{UrFp7YG&sGN z2Vd|?YfWoSM87<}&z^{WHKHF!^tXriD5C!s(VuFq=~`<_iFLfg2W({4Y3DV3!h_7Z zQ%XF?6`aqk-(jhgID;3sgNFA5Tn2diZ!o|2U=z!k^)+tdXDP9dH@Kf!pOz9MT%L!a zi|9{cgq6&CZDQ#D2>Y3JuawxuMO>JNagYJLMK={0gm!TW=Q8V$ML%?~NTozK*q-1P zp5Q9hGwXN}Jk0=P)`sgC<15y1_FwQH4rXk_Hl8h-dA@!DHO8Q#1R#?V00000NkvXX Hu0mjf#jviJ delta 337 zcmV-X0j~b_1Kk6Vm47oyL_t(Ijir-4O9DU`$A8+=9z_i;;r*Y@8bykT#uUsczd{hv z5=0Jy1q~Vv(Gcy?7W4u721RXmGzIPrHDAjWN-Cxg9Cr`g|LtBfF-9h->_q5s2+Y(p zQ^!n2fXsg)n5hM90GB`=*d5t{nc6@ZC;|__46ry7K~gROd4FIIcmo>VhnsBIrb9Co z1}JX>E5HKq3becr7m&`tOlQCjFb&ka5072o3a|{!0#88O`*89jkdfRzu;+cqnW+NY z0$adlBF(3~FTlC?q1o*)3%mnUW-53e?tvO`2dsBxeoo3gU;tu_NUkKgFUhT@em>1h zlKYTcJ;q4C@k+apTvc+Pl3PoCE`g5+fn$ul56Mhb;2LNEN5GZ$;dICi^M_G#Wyy6U jcQ8!3uY+-F;@|iU9k#kF&YETH00000NkvXXu0mjf;4hO9 diff --git a/public/img/icons/error.png b/public/img/icons/error.png index 1e6a102c2c358b8b2f684a07b45d55cb4dd87e1e..62458999f89ed83086512a3a09d7dfbc92b30de8 100644 GIT binary patch delta 393 zcmV;40e1e91e64jm49kUL_t(Ijiu5tYt%sy1@PZLxdTB+EG(=9!Q1IA#74BRvQP^f z#aabHP*GG6BB)jT7+TqQXdsP+mI14P?pi6N4~Z6vuokz0OL93B2NvGEeKY%JnXcCA ze`03ZWkh-!krtd8E!D_=xDGiny{*=<-%1-?@2N7u&3wYS5Q#jr5 zeJnK1o+DhvM$Tmzk^0!cW<=`eTwdb`u1{MH zmj5!^gEtM!xqmF<8*XhCb+uLzX%3%pKIgJB=4u{Y&Sia!JBe2~nseFQa&Q+PcNF)q zhP8$@7ay>SJ1`TGj^HA0jDJqf%RpH`;whEBw9X`^R|r nX|&$`9uD9b&iJ>e`1%K(nT^lgNzbML0000oOuOMx>>^G~I3vBGMVWM2AZ`m*r`?{vO4j;5I6jaDRH5XzB9 zvCuTTj&L2{b1uUs+`vXe>g8Ns;1_OAEfA6Rwa#;m>S2G&ao0J_H|-7wckwCb^17Wj z@dj@jmUDT9FMqf*e$>@kMWloHh>JOw)d^RJ(dAsePH<=O94B%vKgJI3 zVa>%mtl=KaM5N=mj9ZhRlXKa`5W|Ki;|8uoq+?x5gH?RPL(CfOKBE=>+0F+hc=&C! k-m`ri!bzO-Z&C6258|ec@E#xJ1ONa407*qoM6N<$f`|IPt^fc4 diff --git a/public/img/icons/flapping.png b/public/img/icons/flapping.png index 3c2a003d56b3647f283341c678920592cb4540c8..4b253e09ca013633d18b0c8d6d62291510c28110 100644 GIT binary patch delta 482 zcmV<80UiGJ1nmTnm4CxYL_t(Ijg`~Si_bwA2k_VTYsK27kl6CO{mv*`a#DWmmXy?% z3kNoB?Y3(FfIWzu9JY;!1I58Xa&fRyO7f!>H%SgN$)+fm-Gd@0ABUZfzB#R@re@yr z%slV+`DyOZ8nC#Wj4x z7rZX{v-pmIMhLC_MZz#TvA)bF(2WgvfC|UTrI+wNr99H8s;bu8g%_p5+ER5JPNkHe zl$;st!>yF^Sbt3&q?CVP5UVhPIb5y}ml!V>j@7`wZCqWZjW`?+77f!=)p7m zE-dEDfr*szos#ngJMj{ODdnmWLhA%};7Fkza2OY`5ncFD*!JNlrm!3*o48wyxrgnT zOeufF8EnFBe8yJ{rIcq=%Ev8qC^zvFYl=Z{Q_44ssavy{#5oLgwX26 z=0b5MrTnYiq!>dlmX`hdcx2(7jHZ+yEcz#w-*`QQ)?5g!LpAod3yX;uLnnGt%AacN Y4|)^Z&S~Xlf&c&j07*qoM6N<$g6ppH_5c6? delta 490 zcmVeHiPo1b-J(D#tQgz&*_3H`=fg z`*8!C(9?n=xR1xU)xh7>=R?y+u`3Vhj)P4IgmgRF?2Q6Kg6dP!-2U#5kkGdQ@qpIg&kOnLs^f@*@lr^dmiUf zDu-*B!WWHsPvwzq$BUH8ju^u#jh$%0^OVY~F@_Ng#bX#Y6d}|r+(ajqr&R9CZH;0X zM$wKMo~2Yy=KLFU)^I1YF2a?jeox`L#=`tTD;~udx^WvnvA$T~HIAbTV>q5tIlItn ge1f;=NvWKLzmBo1TXS{!S^xk507*qoM6N<$g4&h!Gynhq diff --git a/public/img/images/acknowledgement.png b/public/img/images/acknowledgement.png deleted file mode 100644 index eb03d2db9fd1aa0a8b86c45d4fd31cbbda8e12fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 501 zcmVT(!FYwK@`UE-y1AVA>fLztJuQ?pK#uz0aPA zel?;WNA$Oc_b8(O7SW$-t?628N{My6!v}0+)@kQ8e8Pjwx>HI##}%BY3J zuawxuMO>JNagYJLMK={0gm!TW=Q8V$ML%?~NTozK*q-1Pp5Q9hGwXN}Jk0=P)`sgC r<15y1_FwQH4rXk_Hl8h-dA@!DHO8Q#1R#?V00000NkvXXu0mjfW){$H diff --git a/public/img/images/comment.png b/public/img/images/comment.png deleted file mode 100644 index b7d88a0ace463d9d9df4fc6c1ecf8ae4fba7fd05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 491 zcmVL(oc)dQ4|O8&u^L%i`hw7iF@u=DT_gonzEf_H(tY#vQvuPY@}x46)3V$ zY$*#HCU(wkWNBk#V?nwWeovm6H1ja0?&93;_n!0pGxXl=rRMg8#{6_sYaGBb&ZA-h z1HK~h5>GPQw+UvLP^dLlv5L=ljaT?IzB`7qID?~D%WU^R?_ES$k0=>Y&h_5ybRx>d zh_V?`)_d=Uh;nCK`($SOJ`3+!V?Q?VSVZX&j`*E{E4YeF{sieGUf^M7`!d07N#fWa!c#oMQfAxSiPH8D3LfEZX8W+494nJ za0`JjZQG2Dvly;50{sg z1Q>i!TGMp)&q-e26y7}??@wk=a7|=oK6}Q+W%H}q0wUq9lBXA}zP{{j<42d$Rep1S z?c44*wb)F=uuWKI+82MXuMY$69_l%*ob%9}Z-Z=d#LV4}H|IZ)sXtv+xT3&hF=I%? z>ow{7Ohn|1-$x@M8;p02{4#7ns|f@~#4Z$-~2<2%0NQ1MCq!v5{=lnvW% zYqj+D)W-b&6ty(OzHI3+iEGtnWrf$AVswh4Pdex9dj3Cqm4KPwhQ&WuZA<9kmYMoT zqPikqG|S>{h-G@JN0h1L>52pECtbgx;}candCIHnf=fd7fBU2NVCmr}tp1u;|Nq*T zcJ|4nJ za0`Jj-fdK@bMu=N>M$1}yvtDJ*uj6G8ugaJdGo#0XZ3U}qgGg=lFkw6ON0 zQqUh@lvFmNf|h%FZ!jQ6u(EJ1a;}^NP42)fyR+~6?(8r#sI}@_&M}7@xQzKO`~^>O zx0H6Pk00~}a*j*5g$=CX6B?YrxD&ixO8e|Dsn)92Dk9y9NE;DpG>z@{h;%k0eT_&f z)7bRlVno`ENJsuh?ujf!q^*dwRBJVeNC)u_*Gg&Mn!$38F&^S{(-mwoD;Y{@*D%5I z5La=(r+#1`@~>|D3fCJP#pjMZ!neJ=!@vg|Yr4blS<~;?aJo!z60b{X-{c&JX8gh_ zOon)jMXc@u>}O*Q_lUGGLn9rJNIxUeg<7koOZx>L z;A75lZjYDG<2~+`(!S*1!?c1;tasqXcELtn+BZvSpHDeE1Uiv9EaQru4nJ za0`JjReY2pud27?%7X5YCtUC4974EPs zu!zg|S8$H2nDpLz(Y35MKAF4bUHal?Kl@5lucYPj$ra7P71Dw067R5CE*GsiB>d)q z?7j!TK5^{n>aUOre7EI?)47-CcKWi-*QF;k}f8%dHH}Ct0l`cDPo__yW>zvz^ z#XEJM%(?b4k*}`fw1s$J--G#LVh`AP=l5&0$L=#Zkj-m9yLtPDo5wfU9H{2aKd)JB zyr@Px%x2}*Qvfc diff --git a/public/img/images/error.png b/public/img/images/error.png deleted file mode 100644 index 62458999f89ed83086512a3a09d7dfbc92b30de8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 532 zcmV+v0_**WP)L(lKk)K@bJ--#@tnK}akttOUW^=`F-Yw6L;J3me5+1wl|zR1hMlRs0xQ z*?4FmjfIv0tAOrWDWngH7K*SIw}DG?ITQyL-n@M?`(~N0*6M#^X4+*$dK!@yr)j#q z97LoOc#94faxU*D>3UO|KgUf}EaLbi(ZmrV(penB3%ta^h;;6+8{iop=Un7m;7Q|l zN3C_+NojC7rNL^$ODPSO8vc~h;L1+@HU|-D77KXTs8cxI@O>;a&7LD%#YWC$7?Jwe zz-C11=UiUn2d+SJ1`TGj^HA0 zjDJqf%RpH`;whEBw9X`^R|rX|&$`9uD9b&iJ>e`1%K( WnT^lgNzbML00005)6a{~K^O<{*Y|71+NF@#^1J=cC|hz;e(aW%)RqeeHf`;;YX5*eh@2d@ zjfexq!9j9yuu@9$qZKzv4l~K7D3{%XA}1e*osYgbt*54D-t){n@AvzeQB{@2ju2Ws zA+-A2v8Mlvi+CDi7^;De7{gxlqJf7g<@tIu#?U}NmZ6C~n7~wwVP#7BMh9NuIXWg+l3dU!rD@G8&0K^pOl;#?8B{;@>oqBq?CVP z5UVhPIb5y}ml!V>j@7`wZCqWZjW`?+77f!=)p7mE-dEDfr*szos#ngJMj{O zDdnmWLhA%};7Fkza2OY`5ncFD*!JNlrm!3*o48wyxrgnTOeufF8EnFBe8yJ{rIcq= z%Ev8qC^zvFYl=Z{Q_44ssk4~GISiCBUZfnr7F=zF(CWqJLUAUg{Hxuh7(*|Xmi_y9 zWZ|8Rrj#En`X`p(cs+#HTnMd0HTJg)i-{ORCwfxKpK9z6dK25uY2{{u00000NkvXX Hu0mjfTF@0O diff --git a/public/img/images/in_downtime.png b/public/img/images/in_downtime.png deleted file mode 100644 index d609df62e332dc928542da1f18ad85d60e747e6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 490 zcmVdHQ5XgA-Jjbt;>a7;>|ASUCcMm!- zhG7J(VgEL9#%HX>7*0@EAxxa1!k3il_xCUmWB7nYTvj1Wbg_;f7{q$gGR5*=&(ocxjRTKsA&u@}4#yFopbL!=Z^ZmS4zBq z1^k*>k9y82C9YsSwq@3TM-kmP&SDL=;#_7u`9I`oti)my*}npp@ONgNEG6E+@1?|e zX8m+PgjuZ0tnW5#9>0v?XKXAbHf7dpcpC>wiO&YbXIkYY@-A$_7o+CEF&xGA%zC4g z*o5EkHQvPP%sSP`@8BZ#W!C92$gC%D2A4~T*E8#N?7(*18jvSi$A>cOH0b;O@r~#{ zkLa#PbQ^|vdz*;p?niVV4e?Rm_fI>0vQ^)kS(mVvaTOnD)^CRY$CgPe@Yhq=1N_-W zdg46MK`AkV6?iYRJ}4z#!9SVxekt*4Gj=Ys9(hKfl$ga!nRV~L?eB1-3EpZV3r*zk z;}P11jbn1)_Cdbntr7|^&=+@#qUi4_|j%U_ymq|x!dpG_- vhYy!lc^p6CtIT?OIr2c{0`_2YX1((tPzUQN{0FNI00000NkvXXu0mjf!6zy~ diff --git a/public/img/images/save.png b/public/img/images/save.png deleted file mode 100644 index 2db271920d585eaeffc667dd78df6160689859ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 506 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ za0`JjLLym#5l7t?1mG){EDeTV4W0?? z5oX0-onm&F7MFW2Uv-ADS<@lo*}8^Vt6C1Ut_tBQ+<06%j`>@Iw7x6Dl+dL~&-l*V z*|>^_Ii2;EDU-~soxJx$rq(80*__1?*^r(P8@es{pVEfcd&NI9T(vk_k=2^@=RxIL z24iuiXvqgYTNy)AIbNK4nLl&q@|cEIGK`xKWD8vkX8E$})Yh5NXT1%0br|?9In^3I z`)QjrE}QxL#H_HX-*-HBe;LcX!20>?m;X+f3%ydm@PZ-ZL-p4D4||L)3Hk(K@i9BIqkiviv(z{z&`-*5`3JW11B>#wz&dL<_f`5S|MXIE- z2m~<%6i2CGBw=?tm?(ED?R+gn;c+K z>|`NKmsr8~I{xQr##rU}mQsH3fZN3w{;0}^&OOQNF^1KayVC^RCGOx2-o_Xv z+wmIkw;@=KF}!c&yuuG0VXfi6ya0jz|$JAh&@OtFJcT+RmnX(ODXTRdIIzKT=(E2rMyTf zFDl46wy=zM&89W-E};GgqwVfuvwH8R|HS&58c?*ODdi`4d5gHOsR8pbhDQe9g%Wez mEYpGkK2^*9VK*Cgov#4So@B%YS!sX(00004nJ za0`JjYhUAuyT`&ebV!&M@r;QSn9lm$I4lM!VYrEeQen+;k@1xsKl`C9e;{s_zwS- zR^5hEZXEIRy1Im8q5UDTd(xNmSz>z%=LCHXh?YtcZB}ZjJEoWyEg~YZJj6G>*|5#~ zjrpeOSu1lh&Wb92@y-fdK@bMu=N_(Bf_8p{6c#(D6G8ug5M#hfP_a@1{)GraglK7MYdI2#~Zs7s8u!*l|a1M**z=t_^FaL@9zSsA?i1a8TZAGMoS!`$#=}JWU5s}tr zu^DkAB5g;cQv<<5M7lhP%}1nZM7rMhy-`Fuj!(FobN8|O&Jp}B+oL&myA`*HC%D*@ zf=z1itUmZvY*Z6Dcbk}CrNJ`Z+8=$f{qPRU4Nl|R0ZE793(kzoX77O8k5&E<i*gp*irN^h^? zP20wNmA8k_#r7)kS-i&moV%U6!`4bSUOl9huSKMvbt~^TOl#Q2hjM#U3bs(5y_$3P rYNi<-FwQHuL(l3ZrQ4q)R&s#(^Y*?OUx0vpXY*eG&s>z~)3kJ7{Agf6%f`WfAX%aE0 z#jYT1Sy&K@f?=*%i&fBe4ezczpO<|u4xI9xncvJg=QrxQ&i}-|A($B}xQ3%WdWYLp z?YG(5g@NYfzF`s9aRZ03of%92vcWn&RJChUY%?>qu!eiPZh%AhIKV$)dF~bP69)(Q zLZ6!}@Ezj;KJIgK1>WFzX8g`&#(o^b+qnW&?G}FE{1m=~FIDaP+4?~r;1OII-L0ffzISh3B}x d$L5`Q@e2jFb Date: Wed, 25 Sep 2013 14:04:42 +0200 Subject: [PATCH 03/12] Implement base filter library and tests refs #4469 --- application/controllers/FilterController.php | 101 ++++++ application/views/scripts/filter/index.phtml | 7 + doc/semantic_search.md | 11 + library/Icinga/Filter/Domain.php | 145 ++++++++ library/Icinga/Filter/Filter.php | 295 ++++++++++++++++ library/Icinga/Filter/FilterAttribute.php | 235 +++++++++++++ library/Icinga/Filter/Query/Node.php | 135 ++++++++ library/Icinga/Filter/Query/Tree.php | 171 ++++++++++ library/Icinga/Filter/QueryProposer.php | 65 ++++ library/Icinga/Filter/Type/BooleanFilter.php | 236 +++++++++++++ library/Icinga/Filter/Type/FilterType.php | 100 ++++++ library/Icinga/Filter/Type/TextFilter.php | 211 ++++++++++++ .../Icinga/Filter/Type/TimeRangeSpecifier.php | 215 ++++++++++++ .../controllers/ListController.php | 2 +- .../Backend/Ido/Query/StatusQuery.php | 1 - .../Filter/Backend/IdoQueryConverter.php | 45 +++ .../Monitoring/Filter/MonitoringFilter.php | 108 ++++++ .../Monitoring/Filter/Type/StatusFilter.php | 321 ++++++++++++++++++ .../Monitoring/Filter/UrlViewFilter.php | 213 ++++++++++++ .../library/Filter/Type/StatusFilterTest.php | 142 ++++++++ .../php/library/Filter/UrlViewFilterTest.php | 179 ++++++++++ public/js/icinga/components/semanticsearch.js | 136 ++++++++ test/php/library/Icinga/Filter/DomainTest.php | 102 ++++++ test/php/library/Icinga/Filter/FilterTest.php | 297 ++++++++++++++++ .../Icinga/Filter/QueryHandlerTest.php | 141 ++++++++ .../Icinga/Filter/Type/BooleanFilterTest.php | 153 +++++++++ .../Icinga/Filter/Type/TextSearchTest.php | 76 +++++ .../Filter/Type/TimeRangeSpecifierTest.php | 68 ++++ 28 files changed, 3909 insertions(+), 2 deletions(-) create mode 100644 application/controllers/FilterController.php create mode 100644 application/views/scripts/filter/index.phtml create mode 100644 doc/semantic_search.md create mode 100644 library/Icinga/Filter/Domain.php create mode 100644 library/Icinga/Filter/Filter.php create mode 100644 library/Icinga/Filter/FilterAttribute.php create mode 100644 library/Icinga/Filter/Query/Node.php create mode 100644 library/Icinga/Filter/Query/Tree.php create mode 100644 library/Icinga/Filter/QueryProposer.php create mode 100644 library/Icinga/Filter/Type/BooleanFilter.php create mode 100644 library/Icinga/Filter/Type/FilterType.php create mode 100644 library/Icinga/Filter/Type/TextFilter.php create mode 100644 library/Icinga/Filter/Type/TimeRangeSpecifier.php create mode 100644 modules/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php create mode 100644 modules/monitoring/library/Monitoring/Filter/MonitoringFilter.php create mode 100644 modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php create mode 100644 modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php create mode 100644 modules/monitoring/test/php/library/Filter/Type/StatusFilterTest.php create mode 100644 modules/monitoring/test/php/library/Filter/UrlViewFilterTest.php create mode 100644 public/js/icinga/components/semanticsearch.js create mode 100644 test/php/library/Icinga/Filter/DomainTest.php create mode 100644 test/php/library/Icinga/Filter/FilterTest.php create mode 100644 test/php/library/Icinga/Filter/QueryHandlerTest.php create mode 100644 test/php/library/Icinga/Filter/Type/BooleanFilterTest.php create mode 100644 test/php/library/Icinga/Filter/Type/TextSearchTest.php create mode 100644 test/php/library/Icinga/Filter/Type/TimeRangeSpecifierTest.php diff --git a/application/controllers/FilterController.php b/application/controllers/FilterController.php new file mode 100644 index 000000000..ed9761b70 --- /dev/null +++ b/application/controllers/FilterController.php @@ -0,0 +1,101 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +use Icinga\Web\Form; +use Icinga\Web\Controller\ActionController; +use Icinga\Filter\Filter; +use Icinga\Filter\FilterAttribute; +use Icinga\Filter\Type\TextFilter; +use Icinga\Application\Logger; +use Icinga\Module\Monitoring\Filter\Type\StatusFilter; +use Icinga\Module\Monitoring\Filter\UrlViewFilter; +use Icinga\Web\Url; + +class FilterController extends ActionController +{ + /** + * @var Filter + */ + private $registry; + + public function indexAction() + { + $this->registry = new Filter(); + $filter = new UrlViewFilter(); + + $this->view->form = new Form(); + $this->view->form->addElement( + 'text', + 'query', + array( + 'name' => 'query', + 'label' => 'search', + 'type' => 'search', + 'data-icinga-component' => 'app/semanticsearch', + 'data-icinga-target' => 'host', + 'helptext' => 'Filter test' + ) + ); + $this->view->form->addElement( + 'submit', + 'btn_submit', + array( + 'name' => 'submit' + ) + ); + $this->setupQueries(); + $this->view->form->setRequest($this->getRequest()); + + + if ($this->view->form->isSubmittedAndValid()) { + $tree = $this->registry->createQueryTreeForFilter($this->view->form->getValue('query')); + $this->view->tree = new \Icinga\Web\Widget\FilterBadgeRenderer($tree); + + } else if ($this->getRequest()->getHeader('accept') == 'application/json') { + + $this->getResponse()->setHeader('Content-Type', 'application/json'); + $this->_helper->json($this->parse($this->getRequest()->getParam('query', ''))); + } + } + + private function setupQueries() + { + $this->registry->addDomain(\Icinga\Module\Monitoring\Filter\MonitoringFilter::hostFilter()); + } + + private function parse($text) + { + try { + return $this->registry->getProposalsForQuery($text); + } catch (\Exception $exc) { + Logger::error($exc); + } + } + + +} diff --git a/application/views/scripts/filter/index.phtml b/application/views/scripts/filter/index.phtml new file mode 100644 index 000000000..1bcdb6a43 --- /dev/null +++ b/application/views/scripts/filter/index.phtml @@ -0,0 +1,7 @@ +form; + +if ($this->tree) { + echo $this->tree->render($this); +} \ No newline at end of file diff --git a/doc/semantic_search.md b/doc/semantic_search.md new file mode 100644 index 000000000..df02a3559 --- /dev/null +++ b/doc/semantic_search.md @@ -0,0 +1,11 @@ + + +All critical hosts starting with 'MySql' +All services with status warning that have been checked in the last two days +Services with open Problems and with critical hosts + +with services that are not ok + + +[(SUBJECT)] [(SPECIFIED)] [(OP)] [FILTER] [(ADDITIONAL)] + diff --git a/library/Icinga/Filter/Domain.php b/library/Icinga/Filter/Domain.php new file mode 100644 index 000000000..e5e5ca197 --- /dev/null +++ b/library/Icinga/Filter/Domain.php @@ -0,0 +1,145 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Filter; + +use Icinga\Filter\Query\Node; + +/** + * A Filter domain represents an object that supports filter operations and is basically a + * container for filter attribute + * + */ +class Domain extends QueryProposer +{ + /** + * The label to filter for + * + * @var string + */ + private $label; + + /** + * @var array + */ + private $attributes = array(); + + /** + * Create a new domain identified by the given label + * + * @param $label + */ + public function __construct($label) + { + $this->label = trim($label); + } + + /** + * Return true when this domain handles a given query (even if it's incomplete) + * + * @param String $query The query to test this domain with + * @return bool True if this domain can handle the query + */ + public function handlesQuery($query) + { + $query = trim($query); + return stripos($query, $this->label) === 0; + } + + /** + * Register an attribute to be handled for this filter domain + * + * @param FilterAttribute $attr The attribute object to add to the filter + * @return self Fluent interface + */ + public function registerAttribute(FilterAttribute $attr) + { + $this->attributes[] = $attr; + return $this; + } + + /** + * Return proposals for the given query part + * + * @param String $query The part of the query that this specifier should parse + * @return array An array containing 0..* proposal text tokens + */ + public function getProposalsForQuery($query) + { + $query = trim($query); + if ($this->handlesQuery($query)) { + // remove domain portion of the query + $query = trim(substr($query, strlen($this->label))); + } + + $proposals = array(); + foreach ($this->attributes as $attributeHandler) { + $proposals = array_merge($proposals, $attributeHandler->getProposalsForQuery($query)); + } + + return $proposals; + } + + /** + * Return the label identifying this domain + * + * @return string the label for this domain + */ + public function getLabel() + { + return $this->label; + } + + /** + * Create a query tree node representing the given query and using the field given as + * $leftOperand as the attribute (left leaf of the tree) + * + * @param String $query The query to create the node from + * @param String $leftOperand The attribute use for the node + * @return Node|null + */ + public function convertToTreeNode($query) + { + if ($this->handlesQuery($query)) { + // remove domain portion of the query + $query = trim(substr($query, strlen($this->label))); + } + + foreach ($this->attributes as $attributeHandler) { + if ($attributeHandler->isValidQuery($query)) { + $node = $attributeHandler->convertToTreeNode($query); + if ($node) { + $node->context = $this->label; + } + return $node; + } + } + return null; + } +} \ No newline at end of file diff --git a/library/Icinga/Filter/Filter.php b/library/Icinga/Filter/Filter.php new file mode 100644 index 000000000..77fee5035 --- /dev/null +++ b/library/Icinga/Filter/Filter.php @@ -0,0 +1,295 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Filter; + + +use Icinga\Filter\Query\Tree; +use Icinga\Filter\Query\Node; + +/** + * Class for filter input and query parsing + * + * This class handles the top level parsing of queries, i.e. + * - Splitting queries at conjunctions and parsing them part by part + * - Delegating the query parts to specific filter domains handling this filters + * - Building a query tree that allows to convert a filter representation into others (url to string, string to url, sql..) + * + * Filters are split in Filter Domains, Attributes and Types: + * + * Attribute + * Domain | FilterType + * _|__ _|_ ______|____ + * / \/ \/ \ + * Host name is not 'test' + * + */ +class Filter extends QueryProposer +{ + /** + * The default domain to use, if not set the first added domain + * + * @var null + */ + private $defaultDomain = null; + + /** + * An array containing all query parts that couldn't be parsed + * + * @var array + */ + private $ignoredQueryParts = array(); + + /** + * An array containing all domains of this filter + * + * @var array + */ + private $domains = array(); + + /** + * Create a new domain and return it + * + * @param String $name The field to be handled by this domain + * + * @return Domain The created domain object + */ + public function createFilterDomain($name) + { + $domain = new Domain(trim($name)); + + $this->domains[] = $domain; + return $domain; + } + + /** + * Set the default domain (used if no domain identifier is given to the query) to the given one + * + * @param Domain $domain The domain to use as the default. Will be added to the domain list if not present yet + */ + public function setDefaultDomain(Domain $domain) + { + if (!in_array($domain, $this->domains)) { + $this->domains[] = $domain; + } + $this->defaultDomain = $domain; + } + + /** + * Return the default domaon + * + * @return Domain Return either the domain that has been explicitly set as the default domain or the first + * added. If no domain has been added yet null is returned + */ + public function getDefaultDomain() + { + if ($this->defaultDomain !== null) { + return $this->defaultDomain; + } else if (count($this->domains) > 0) { + return $this->domains[0]; + } + return null; + } + + /** + * Add a domain to this filter + * + * @param Domain $domain The domain to add + * @return self Fluent interface + */ + public function addDomain(Domain $domain) + { + $this->domains[] = $domain; + return $this; + } + + /** + * Return all domains that could match the given query + * + * @param String $query The query to search matching domains for + * + * @return array An array containing 0..* domains that could handle the query + */ + public function getDomainsForQuery($query) + { + $domains = array(); + foreach ($this->domains as $domain) { + if ($domain->handlesQuery($query)) { + $domains[] = $domain; + } + } + return $domains; + } + + /** + * Return the first domain matching for this query (or the default domain) + * + * @param String $query The query to search for a domain + * @return Domain A matching domain or the default domain if no domain is matching + */ + public function getFirstDomainForQuery($query) + { + $domains = $this->getDomainsForQuery($query); + if (empty($domains)) { + $domain = $this->getDefaultDomain(); + } else { + $domain = $domains[0]; + } + return $domain; + } + + /** + * Return proposals for the given query part + * + * @param String $query The part of the query that this specifier should parse + * + * @return array An array containing 0..* proposal text tokens + */ + public function getProposalsForQuery($query) + { + $query = $this->getLastQueryPart($query); + $proposals = array(); + $domains = $this->getDomainsForQuery($query); + foreach ($domains as $domain) { + $proposals = array_merge($proposals, $domain->getProposalsForQuery($query)); + } + if (empty($proposals) && $this->getDefaultDomain()) { + foreach ($this->domains as $domain) { + if (stripos($domain->getLabel(), $query) === 0 || $query == '') { + $proposals[] = self::markDifference($domain->getLabel(), $query); + } + } + $proposals = array_merge($proposals, $this->getDefaultDomain()->getProposalsForQuery($query)); + } + return $proposals; + } + + /** + * Split the query at the next conjunction and return a 3 element array containing (left, conjunction, right) + * + * @param $query The query to split + * @return array An three element tupel in the form array($left, $conjunction, $right) + */ + private function splitQueryAtNextConjunction($query) + { + $delimiter = array('AND', 'OR'); + $inStr = false; + for ($i = 0; $i < strlen($query); $i++) { + // Skip strings + $char = $query[$i]; + if ($inStr) { + if ($char == $inStr) { + $inStr = false; + } + continue; + } + if ($char === '\'' || $char === '"') { + $inStr = $char; + continue; + } + foreach ($delimiter as $delimiterString) { + $delimiterLength = strlen($delimiterString); + if (strtoupper(substr($query, $i, $delimiterLength)) === $delimiterString) { + // Delimiter, split into left, middle, right part + $nextPartOffset = $i + $delimiterLength; + $left = substr($query, 0, $i); + $conjunction = $delimiterString; + $right = substr($query, $nextPartOffset); + return array(trim($left), $conjunction, trim($right)); + } + } + } + return array($query, null, null); + } + + /** + * Return the last part of the query + * + * Mostly required for generating query proposals + * + * @param $query The query to scan for the last part + * @return mixed An string containing the rightmost query + */ + private function getLastQueryPart($query) + { + $right = $query; + do { + list($left, $conjuction, $right) = $this->splitQueryAtNextConjunction($right); + } while($conjuction !== null); + return $left; + } + + /** + * Create a query tree containing this filter + * + * Query parts that couldn't be parsed can be retrieved with Filter::getIgnoredQueryParts + * + * @param String $query The query string to parse into a query tree + * @return Tree The resulting query tree (empty for invalid queries) + */ + public function createQueryTreeForFilter($query) + { + $this->ignoredQueryParts = array(); + $right = $query; + $domain = null; + $tree = new Tree(); + do { + list($left, $conjunction, $right) = $this->splitQueryAtNextConjunction($right); + $domain = $this->getFirstDomainForQuery($left); + if ($domain === null) { + $this->ignoredQueryParts[] = $left; + continue; + } + + $node = $domain->convertToTreeNode($left); + if (!$node) { + $this->ignoredQueryParts[] = $left; + continue; + } + $tree->insert($node); + + if ($conjunction === 'AND') { + $tree->insert(Node::createAndNode()); + } elseif($conjunction === 'OR') { + $tree->insert(Node::createOrNode()); + } + + } while ($right !== null); + return $tree; + } + + /** + * Return all parts that couldn't be parsed in the last createQueryTreeForFilter run + * + * @return array An array containing invalid/non-parseable query strings + */ + public function getIgnoredQueryParts() + { + return $this->ignoredQueryParts; + } +} \ No newline at end of file diff --git a/library/Icinga/Filter/FilterAttribute.php b/library/Icinga/Filter/FilterAttribute.php new file mode 100644 index 000000000..9927ddf13 --- /dev/null +++ b/library/Icinga/Filter/FilterAttribute.php @@ -0,0 +1,235 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Filter; + +use Icinga\Filter\Query\Node; +use Icinga\Filter\Type\FilterType; + +/** + * Filter attribute class representing one possible filter for a specific domain + * + * These classes contain a Filter Type to determine possible operators/values etc. + * Often the filter class directly contains the attribute and handles field => attribute mapping, + * but one exception is the BooleanFilter, which overwrites the attribute to use for a more convenient. + * + * Basically, this component maps multiple attributes to one specific field. + */ +class FilterAttribute extends QueryProposer +{ + /** + * The FilterType object that handles operations on this attribute + * + * @var Type\FilterType + */ + private $type; + + /** + * An array of attribute tokens to map, or empty to let the filter type choose it's own attribute + * and skip this class + * + * @var array + */ + private $attributes = array(); + + /** + * The field that is being represented by the given attributes + * + * @var String + */ + private $field; + + /** + * Create a new FilterAttribute using the given type as the filter Type + * + * @param FilterType $type The type of this filter + */ + public function __construct(FilterType $type) + { + $this->type = $type; + } + + /** + * Set a list of attributes to be mapped to this filter + * + * @param String $attr An attribute to be recognized by this filter + * @param String ... + * + * @return self Fluent interface + */ + public function setHandledAttributes($attr) + { + if (!$this->field) { + $this->field = $attr; + } + foreach(func_get_args() as $arg) { + $this->attributes[] = trim($arg); + } + return $this; + } + + /** + * Set the field to be represented by this FilterAttribute + * + * The field is always unique while the attributes are ambiguous. + * + * @param String $field The field this Attribute collection maps to + * + * @return self Fluent Interface + */ + public function setField($field) + { + $this->field = $field; + return $this; + } + + /** + * Return the largest attribute that matches this query or null if none matches + * + * @param String $query The query to search for containing an attribute + * + * @return String The attribute to be used or null + */ + private function getMatchingAttribute($query) + { + $query = trim($query); + foreach ($this->attributes as $attribute) { + if (stripos($query, $attribute) === 0) { + return $attribute; + } + } + return null; + } + + /** + * Return true if this query contains an attribute mapped by this object + * + * @param String $query The query to search for the attribute + * + * @return bool True when this query contains an attribute mapped by this filter + */ + public function queryHasSupportedAttribute($query) { + return $this->getMatchingAttribute($query) !== null; + } + + /** + * Return proposals for the given query part + * + * @param String $query The part of the query that this specifier should parse + * + * @return array An array containing 0..* proposal text tokens + */ + public function getProposalsForQuery($query) + { + $query = trim($query); + $attribute = $this->getMatchingAttribute($query); + + if ($attribute !== null || count($this->attributes) == 0) { + $subQuery = trim(substr($query, strlen($attribute))); + return $this->type->getProposalsForQuery($subQuery); + } else { + return $this->getAttributeProposalsForQuery($query); + } + } + + /** + * Return an array of possible attributes that can be used for completing the query + * + * @param String $query The query to fetch completion proposals for + * + * @return array An array containing 0..* strings with possible completions + */ + public function getAttributeProposalsForQuery($query) + { + if ($query === '') { + if (count($this->attributes)) { + return array($this->attributes[0]); + } else { + return $this->type->getProposalsForQuery($query); + } + } + $proposals = array(); + foreach ($this->attributes as $attribute) { + if (stripos($attribute, $query) === 0) { + $proposals[] = self::markDifference($attribute, $query); + break; + } + } + return $proposals; + } + + /** + * Return true if $query is a valid query for this filter, otherwise false + * + * @param String $query The query to validate + * + * @return bool True if $query represents a valid filter for this object, otherwise false + */ + public function isValidQuery($query) + { + $attribute = $this->getMatchingAttribute($query); + if ($attribute === null && count($this->attributes) > 0) { + return false; + } + $subQuery = trim(substr($query, strlen($attribute))); + return $this->type->isValidQuery($subQuery); + } + + /** + * Convert the given query to a tree node + * + * @param String $query The query to convert to a tree node + * + * @return Node The tree node representing this query or null if the query is not valid + */ + public function convertToTreeNode($query) + { + if (!$this->isValidQuery($query)) { + return null; + } + $lValue = $this->getMatchingAttribute($query); + $subQuery = trim(substr($query, strlen($lValue))); + + return $this->type->createTreeNode($subQuery, $this->field); + } + + /** + * Factory method to make filter creation more convenient, same as the constructor + * + * @param FilterType $type The filtertype to use for this attribute + * + * @return FilterAttribute An instance of FilterAttribute + */ + public static function create(FilterType $type) + { + return new FilterAttribute($type); + } + + +} diff --git a/library/Icinga/Filter/Query/Node.php b/library/Icinga/Filter/Query/Node.php new file mode 100644 index 000000000..92c3c9475 --- /dev/null +++ b/library/Icinga/Filter/Query/Node.php @@ -0,0 +1,135 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Filter\Query; + +/** + * Container class for the Node of a query tree + */ +class Node +{ + const TYPE_AND = 'AND'; + const TYPE_OR = 'OR'; + const TYPE_OPERATOR = 'OPERATOR'; + + const OPERATOR_EQUALS = '='; + const OPERATOR_EQUALS_NOT = '!='; + const OPERATOR_GREATER = '>'; + const OPERATOR_LESS = '<'; + const OPERATOR_GREATER_EQ = '>='; + const OPERATOR_LESS_EQ = '<='; + + /** + * Array containing all possible operators + * + * @var array + */ + static public $operatorList = array( + self::OPERATOR_EQUALS, self::OPERATOR_EQUALS_NOT, self::OPERATOR_GREATER, + self::OPERATOR_LESS, self::OPERATOR_GREATER_EQ, self::OPERATOR_LESS_EQ + ); + + /** + * The type of this node + * + * @var string + */ + public $type = self::TYPE_OPERATOR; + + /** + * The operator of this node, if type is TYPE_OPERATOR + * + * @var string + */ + public $operator = ''; + + /** + * The parent of this node or null if no parent exists + * + * @var Node + */ + public $parent; + + /** + * The left element of this Node + * + * @var String|Node + */ + public $left; + + /** + * The right element of this Node + * + * @var String|Node + */ + public $right; + + /** + * Factory method for creating operator nodes + * + * @param String $operator The operator to use + * @param String $left The left side of the node, i.e. target (mostly attribute) to query for with this node + * @param String $right The right side of the node, i.e. the value to use for querying + * + * @return Node An operator Node instance + */ + public static function createOperatorNode($operator, $left, $right) + { + $node = new Node(); + $node->type = self::TYPE_OPERATOR; + $node->operator = $operator; + $node->left = $left; + $node->right = $right; + return $node; + } + + /** + * Factory method for creating an AND conjunction node + * + * @return Node An AND Node instance + */ + public static function createAndNode() + { + $node = new Node(); + $node->type = self::TYPE_AND; + return $node; + } + + /** + * Factory method for creating an OR conjunction node + * + * @return Node An OR Node instance + */ + public static function createOrNode() + { + $node = new Node(); + $node->type = self::TYPE_OR; + return $node; + } +} diff --git a/library/Icinga/Filter/Query/Tree.php b/library/Icinga/Filter/Query/Tree.php new file mode 100644 index 000000000..e3c50717c --- /dev/null +++ b/library/Icinga/Filter/Query/Tree.php @@ -0,0 +1,171 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Filter\Query; + +/** + * A binary tree representing queries in an interchangeable way + * + * This tree should always be created from queries and used to create queries + * or convert query formats. Currently it doesn't support grouped expressions, + * although this can be implemented rather easily in the tree (the problem is more or less + * how to implement it in query languages) + */ +class Tree +{ + /** + * The curretnt root node of the Tree + * + * @var Node|null + */ + public $root; + + /** + * The last inserted node of the Tree + * + * @var Node|null + */ + private $lastNode; + + /** + * Insert a node into this tree, recognizing type and insert position + * + * @param Node $node The node to insert into the tree + */ + public function insert(Node $node) + { + + if ($this->root === null) { + $this->root = $node; + } else { + switch ($node->type) { + case Node::TYPE_AND: + $this->insertAndNode($node, $this->root); + break; + case Node::TYPE_OR: + $this->insertOrNode($node, $this->root); + break; + case Node::TYPE_OPERATOR: + $node->parent = $this->lastNode; + if ($this->lastNode->left == null) { + $this->lastNode->left = $node; + } else if($this->lastNode->right == null) { + $this->lastNode->right = $node; + } + break; + } + } + $this->lastNode = $node; + } + + /** + * Determine the insert position of an AND node, using $currentNode as the parent node + * and insert the tree + * + * And nodes are always with a higher priority than other nodes and only traverse down the tree + * when encountering another AND tree on the way + * + * @param Node $node The node to insert + * @param Node $currentNode The current node context + */ + private function insertAndNode(Node $node, Node $currentNode) + { + + if ($currentNode->type != Node::TYPE_AND) { + // No AND node, insert into tree + if($currentNode->parent !== null) { + $node->parent = $currentNode->parent; + if ($currentNode->parent->left === $currentNode) { + $currentNode->parent->left = $node; + } else { + $currentNode->parent->right = $node; + } + } else { + $this->root = $node; + } + $currentNode->parent = $node; + if ($node->left) { + $currentNode->right = $node->left; + } + $node->left = $currentNode; + $node->parent = null; + return; + + } elseif ($currentNode->left == null) { + // Insert right if there's place + $currentNode->left = $node; + $node->parent = $currentNode; + } elseif ($currentNode->right == null) { + // Insert right if there's place + $currentNode->right = $node; + $node->parent = $currentNode; + } else { + // traverse down the tree if free insertion point is found + $this->insertAndNode($node, $currentNode->right); + + } + } + + /** + * Insert an OR node + * + * OR nodes are always inserted over operator nodes but below AND nodes + * + * @param Node $node The OR node to insert + * @param Node $currentNode The current context to use for insertion + */ + private function insertOrNode(Node $node, Node $currentNode) + { + if ($currentNode->type === Node::TYPE_OPERATOR) { + // Always insert when encountering an operator node + if($currentNode->parent !== null) { + $node->parent = $currentNode->parent; + if ($currentNode->parent->left === $currentNode) { + $currentNode->parent->left = $node; + } else { + $currentNode->parent->right = $node; + } + } else { + $this->root = $node; + } + $currentNode->parent = $node; + $node->left = $currentNode; + } elseif ($currentNode->left === null) { + $currentNode->left = $node; + $node->parent = $currentNode; + return; + } elseif ($currentNode->right === null) { + $currentNode->right = $node; + $node->parent = $currentNode; + return; + } else { + $this->insertOrNode($node, $currentNode->right); + } + } +} diff --git a/library/Icinga/Filter/QueryProposer.php b/library/Icinga/Filter/QueryProposer.php new file mode 100644 index 000000000..ca198d927 --- /dev/null +++ b/library/Icinga/Filter/QueryProposer.php @@ -0,0 +1,65 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Filter; + +/** + * Base class for Query proposers + * + * Query Proposer accept an query string in their getProposalsForQuery method and return + * possible parts to complete this query + */ +abstract class QueryProposer +{ + /** + * Static helper function to encapsulate similar string parts with an {} + * + * @param $attribute The attribute to mark differences in + * @param $query The query to use for determining similarities + * + * @return string The attribute string with similar parts encapsulated in curly braces + */ + public static function markDifference($attribute, $query) + { + if (strlen($query) === 0) { + return $attribute; + } + return '{' . substr($attribute, 0, strlen($query)) . '}' . substr($attribute, strlen($query)); + } + + /** + * Return proposals for the given query part + * + * @param String $query The part of the query that this specifier should parse + * + * @return array An array containing 0..* proposal text tokens + */ + abstract public function getProposalsForQuery($query); + +} \ No newline at end of file diff --git a/library/Icinga/Filter/Type/BooleanFilter.php b/library/Icinga/Filter/Type/BooleanFilter.php new file mode 100644 index 000000000..ce6d816e5 --- /dev/null +++ b/library/Icinga/Filter/Type/BooleanFilter.php @@ -0,0 +1,236 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Filter\Type; + +use Icinga\Filter\Query\Node; + +/** + * Boolean filter for setting flag filters (host is in problem state) + * + */ +class BooleanFilter extends FilterType +{ + /** + * The operqator map to use + * + * @var array + */ + private $operators = array( + Node::OPERATOR_EQUALS => 'Is', + Node::OPERATOR_EQUALS_NOT => 'Is Not' + ); + + /** + * The fields that are supported by this filter + * + * These fields somehow break the mechanismn as they overwrite the field given in the + * Attribute + * + * @var array + */ + private $fields = array(); + + /** + * An TimeRangeSpecifier if a field is given + * + * @var TimeRangeSpecifier + */ + private $subFilter; + + /** + * An optional field to use for time information (no time filters are possible if this is not given) + * + * @var string + */ + private $timeField; + + /** + * Create a new Boolean Filter handling the given field mapping + * + * @param array $fields The fields to use, in a internal_key => Text token mapping + * @param String $timeField An optional time field, allows time specifiers to be appended to the query if given + */ + public function __construct(array $fields, $timeField = false) + { + $this->fields = $fields; + if (is_string($timeField)) { + $this->subFilter = new TimeRangeSpecifier(); + $this->timeField = $timeField; + } + } + + /** + * Overwrite the text to use for operators + * + * @param String $positive The 'set flag' operator (default: 'is') + * @param String $negative The 'unset flag' operator (default: 'is not') + */ + public function setOperators($positive, $negative) + { + $this->operators = array( + Node::OPERATOR_EQUALS => $positive, + Node::OPERATOR_EQUALS_NOT => $negative + ); + } + + /** + * Return a proposal for completing a field given the $query string + * + * @param String $query The query to get the proposal from + * @return array An array containing text tokens that could be used for completing the query + */ + private function getFieldProposals($query) + { + $proposals = array(); + foreach ($this->fields as $key => $field) { + $match = null; + if (self::startsWith($field, $query)) { + $match = $field; + } elseif (self::startsWith($key, $query)) { + $match = $key; + } else { + continue; + } + + if (self::startsWith($query, $match) && $this->subFilter) { + $subQuery = trim(substr($query, strlen($match))); + $proposals = $proposals + $this->subFilter->getProposalsForQuery($subQuery); + } else if (strtolower($query) !== strtolower($match)) { + $proposals[] = self::markDifference($match, $query); + } + } + return $proposals; + } + + /** + * Return proposals for the given query part + * + * @param String $query The part of the query that this specifier should parse + * + * @return array An array containing 0..* proposal text tokens + */ + public function getProposalsForQuery($query) + { + $proposals = array(); + $operators = $this->getOperators(); + if ($query === '') { + return $this->getOperators(); + } + + foreach ($operators as $operator) { + if (strtolower($operator) === strtolower($query)) { + $proposals += array_values($this->fields); + } else if (self::startsWith($operator, $query)) { + $proposals[] = self::markDifference($operator, $query); + } else if (self::startsWith($query, $operator)) { + $fieldPart = trim(substr($query, strlen($operator))); + $proposals = $proposals + $this->getFieldProposals($fieldPart); + } + } + return $proposals; + } + + /** + * Return every possible operator of this Filter type + * + * @return array An array + */ + public function getOperators() + { + return $this->operators; + } + + /** + * Return true when the given query is valid for this type + * + * @param String $query The query to test for this filter type + * @return bool True if the query can be parsed by this filter type + */ + public function isValidQuery($query) + { + list($field, $operator, $subQuery) = $this->getFieldValueForQuery($query); + $valid = ($field !== null && $operator !== null); + if ($valid && $subQuery && $this->subFilter !== null) { + $valid = $this->subFilter->isValidQuery($subQuery); + } + return $valid; + } + + /** + * Return a 3 element tupel with array(field, value, right) from the given query part + * + * @param String $query The query string to use + * @return array An 3 element tupel containing the field, value and optionally the right + * side of the query + */ + public function getFieldValueForQuery($query) + { + $operator = $this->getMatchingOperatorForQuery($query); + if (!$operator) { + return array(null, null, null); + } + $operatorList = array_flip($this->operators); + $query = trim(substr($query, strlen($operator))); + + $operator = $operatorList[$operator]; + foreach ($this->fields as $key => $field) { + if (self::startsWith($query, $field)) { + $subQuery = trim(substr($query, strlen($field))); + return array($key, $operator === Node::OPERATOR_EQUALS ? 1 : 0, $subQuery); + } + } + return array(null, null, null); + } + + /** + * Create a query tree node representing the given query and using the field given as + * $leftOperand as the attribute (left leaf of the tree) + * + * @param String $query The query to create the node from + * @param String $leftOperand The attribute use for the node + * @return Node|null + */ + public function createTreeNode($query, $leftOperand) + { + list($field, $value, $subQuery) = $this->getFieldValueForQuery($query); + if ($field === null || $value === null) { + return null; + } + $node = Node::createOperatorNode(Node::OPERATOR_EQUALS, $field, $value); + if ($this->subFilter && $subQuery && $this->subFilter->isValidQuery($subQuery)) { + $subNode = $this->subFilter->createTreeNode($subQuery, $this->timeField); + $conjunctionNode = Node::createAndNode(); + $conjunctionNode->left = $subNode; + $conjunctionNode->right = $node; + $node = $conjunctionNode; + } + return $node; + } + +} \ No newline at end of file diff --git a/library/Icinga/Filter/Type/FilterType.php b/library/Icinga/Filter/Type/FilterType.php new file mode 100644 index 000000000..6a498894f --- /dev/null +++ b/library/Icinga/Filter/Type/FilterType.php @@ -0,0 +1,100 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Filter\Type; + +use Icinga\Filter\QueryProposer; + +/** + * A specific type of filter + * + * Implementations represent specific filters like text, monitoringstatus, time, flags, etc + * + */ +abstract class FilterType extends QueryProposer +{ + /** + * Return a list containing all operators that can appear in this filter type + * + * @return array An array of strings + */ + abstract public function getOperators(); + + /** + * Return true if the given query is valid for this type + * + * @param String $query The query string to validate + * + * @return boolean True when the query can be converted to a tree node, otherwise false + */ + abstract public function isValidQuery($query); + + /** + * Return a tree node representing the given query that can be inserted into a query tree + * + * @param String $query The query to parse into a Node + * @param String $leftOperand The field to use for the left (target) side of the node + * + * @return Node A tree node + */ + abstract public function createTreeNode($query, $leftOperand); + + /** + * More verbose helper method for testing whether a string starts with the second one + * + * @param String $string The string to use as the haystack + * @param String $substring The string to use as the needle + * + * @return bool True when $string starts with $substring + */ + static public function startsWith($string, $substring) + { + return stripos($string, $substring) === 0; + } + + /** + * Get the operator that matches the given query best (i.e. the one with longest matching string) + * + * @param String $query The query to extract the operator from + * + * @return string The operator contained in this query or an empty string if no operator matches + */ + protected function getMatchingOperatorForQuery($query) + { + $matchingOperator = ''; + foreach ($this->getOperators() as $operator) { + if (stripos($query, $operator) === 0) { + if (strlen($matchingOperator) < strlen($operator) ){ + $matchingOperator = $operator; + } + } + } + return $matchingOperator; + } +} \ No newline at end of file diff --git a/library/Icinga/Filter/Type/TextFilter.php b/library/Icinga/Filter/Type/TextFilter.php new file mode 100644 index 000000000..e4e986665 --- /dev/null +++ b/library/Icinga/Filter/Type/TextFilter.php @@ -0,0 +1,211 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Filter\Type; + + +use Icinga\Filter\Query\Node; + +class TextFilter extends FilterType +{ + /** + * Mapping of possible text tokens to normalized operators + * + * @var array + */ + private $operators = array( + 'Is' => Node::OPERATOR_EQUALS, + 'Is Not' => Node::OPERATOR_EQUALS_NOT, + 'Starts With' => Node::OPERATOR_EQUALS, + 'Ends With' => Node::OPERATOR_EQUALS, + 'Contains' => Node::OPERATOR_EQUALS, + '=' => Node::OPERATOR_EQUALS, + '!=' => Node::OPERATOR_EQUALS_NOT, + 'Like' => Node::OPERATOR_EQUALS, + 'Matches' => Node::OPERATOR_EQUALS + ); + + /** + * Return all possible operator tokens for this filter + * + * @return array + */ + public function getOperators() + { + return array_keys($this->operators); + } + + /** + * Return proposals for the given query part + * + * @param String $query The part of the query that this specifier should parse + * + * @return array An array containing 0..* proposal text tokens + */ + public function getProposalsForQuery($query) + { + $proposals = array(); + $operators = $this->getOperators(); + if ($query === '') { + return $this->getOperators(); + } + foreach ($operators as $operator) { + if (strtolower($operator) === strtolower($query)) { + $proposals += array('\'' . $this->getProposalsForValues($operator) . '\''); + } else if (self::startsWith($operator, $query)) { + $proposals[] = self::markDifference($operator, $query); + } + } + + return $proposals; + } + + /** + * Return a (operator, value) tupel representing the given query or (null, null) if + * the input is not valid + * + * @param String $query The query part to extract the operator and value from + * @return array An array containg the operator as the first item and the value as the second + * or (null, null) if parsing is not possible for this query + */ + public function getOperatorAndValueFromQuery($query) + { + $matchingOperator = $this->getMatchingOperatorForQuery($query); + + if (!$matchingOperator) { + return array(null, null); + } + $valuePart = trim(substr($query, strlen($matchingOperator))); + if ($valuePart == '') { + return array($matchingOperator, null); + } + $this->normalizeQuery($matchingOperator, $valuePart); + return array($matchingOperator, $valuePart); + } + + /** + * Return true when the given query is valid for this type + * + * @param String $query The query to test for this filter type + * @return bool True if the query can be parsed by this filter type + */ + public function isValidQuery($query) + { + list ($operator, $value) = $this->getOperatorAndValueFromQuery($query); + return $operator !== null && $value !== null; + } + + /** + * Create a query tree node representing the given query and using the field given as + * $leftOperand as the attribute (left leaf of the tree) + * + * @param String $query The query to create the node from + * @param String $leftOperand The attribute use for the node + * @return Node|null + */ + public function createTreeNode($query, $leftOperand) + { + list ($operator, $value) = $this->getOperatorAndValueFromQuery($query); + if ($operator === null || $value === null) { + return null; + } + $node = new Node(); + $node->type = Node::TYPE_OPERATOR; + $node->operator = $operator; + $node->left = $leftOperand; + $node->right = $value; + return $node; + } + + /** + * Normalize the operator and value for the given query + * + * This removes quotes and adds wildcards for specific operators. + * The operator and value will be modified in this method and can be + * added to a QueryNode afterwards + * + * @param String $operator A reference to the operator string + * @param String $value A reference to the value string + */ + private function normalizeQuery(&$operator, &$value) + { + $value = trim($value); + + if ($value[0] == '\'' || $value[0] == '"') { + $value = substr($value, 1); + } + $lastPos = strlen($value) - 1; + if ($value[$lastPos] == '"' || $value[$lastPos] == '\'') { + $value = substr($value, 0, -1); + } + + switch (strtolower($operator)) { + case 'starts with': + $value = '*' . $value; + break; + case 'ends with': + $value = $value . '*'; + break; + case 'matches': + case 'contains': + $value = '*' . $value . '*'; + break; + } + foreach ($this->operators as $operatorType => $type) { + if (strtolower($operatorType) === strtolower($operator)) { + $operator = $type; + } + } + } + + /** + * Return generic value proposals for the given operator + * + * @param String $operator The operator string to create a proposal for + * @return string The created proposals + */ + public function getProposalsForValues($operator) + { + switch (strtolower($operator)) { + case 'starts with': + return 'value...'; + case 'ends with': + return '...value'; + case 'is': + case 'is not': + case '=': + case '!=': + return 'value'; + case 'matches': + case 'contains': + case 'like': + return '...value...'; + } + } +} \ No newline at end of file diff --git a/library/Icinga/Filter/Type/TimeRangeSpecifier.php b/library/Icinga/Filter/Type/TimeRangeSpecifier.php new file mode 100644 index 000000000..5b511160c --- /dev/null +++ b/library/Icinga/Filter/Type/TimeRangeSpecifier.php @@ -0,0 +1,215 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Filter\Type; +use Icinga\Filter\Query\Node; + +/** + * Filter Type for specifying time points. Uses valid inputs for strtotime as the + * Filter value + * + */ +class TimeRangeSpecifier extends FilterType +{ + private $forcedPrefix = null; + + /** + * Default operator to use + * + * @var array A Text Token => Operator mapping for every supported operator + */ + private $operator = array( + 'Since' => Node::OPERATOR_GREATER_EQ, + 'Before' => Node::OPERATOR_LESS_EQ + ); + + + + /** + * Example values that will be displayed to the user + * + * @var array + */ + public $timeExamples = array( + '"5 minutes"', + '"30 minutes"', + '"1 hour"', + '"6 hours"', + '"1 day"', + '"yesterday"', + '"last Monday"' + ); + + /** + * Return proposals for the given query part + * + * @param String $query The part of the query that this specifier should parse + * @return array An array containing 0..* proposal text tokens + */ + public function getProposalsForQuery($query) + { + if ($query === '') { + return $this->getOperators(); + } + $proposals = array(); + foreach ($this->getOperators() as $operator) { + if (self::startsWith($query, $operator)) { + if (!trim(substr($query, strlen($operator)))) { + $proposals = array_merge($proposals, $this->timeExamples); + } + } elseif (self::startsWith($operator, $query)) { + $proposals[] = self::markDifference($operator, $query); + } + } + return $proposals; + } + + /** + * Return an array containing the textual representation of all operators represented by this filter + * + * @return array An array of operator string + */ + public function getOperators() + { + return array_keys($this->operator); + } + + + /** + * Return a two element array with the operator and the timestring parsed from the given query part + * + * @param String $query The query to extract the operator and time value from + * @return array An array containing the operator as the first and the string for strotime as the second + * value or (null,null) if the query is invalid + */ + private function getOperatorAndTimeStringFromQuery($query) + { + $currentOperator = null; + foreach ($this->operator as $operator => $type) { + if (self::startsWith($query, $operator)) { + $currentOperator = $type; + $query = trim(substr($query, strlen($operator))); + break; + } + } + $query = trim($query, '\'"'); + if (!$query || $currentOperator === null) { + return array(null, null); + } + + if (is_numeric($query[0])) { + if($this->forcedPrefix) { + $prefix = $this->forcedPrefix; + } elseif($currentOperator === Node::OPERATOR_GREATER_EQ) { + $prefix = '-'; + } else { + $prefix = '+'; + } + $query = $prefix . $query; + } + + if (!strtotime($query)) { + return array(null, null); + } + return array($currentOperator, $query); + } + + /** + * Return true if the query is valid, otherwise false + * + * @param String $query The query string to validate + * @return bool True if the query is valid, otherwise false + */ + public function isValidQuery($query) + { + list($operator, $timeQuery) = $this->getOperatorAndTimeStringFromQuery($query); + return $timeQuery !== null; + } + + /** + * Create a query tree node representing the given query and using the field given as + * $leftOperand as the attribute (left leaf of the tree) + * + * @param String $query The query to create the node from + * @param String $leftOperand The attribute use for the node + * @return Node|null + */ + public function createTreeNode($query, $leftOperand) + { + list($operator, $timeQuery) = $this->getOperatorAndTimeStringFromQuery($query); + + if ($operator === null || $timeQuery === null) { + return null; + } + return Node::createOperatorNode($operator, $leftOperand, $timeQuery); + } + + /** + * Set possible operators for this query, in a 'stringtoken' => NodeOperatorConstant map + * + * @param array $operator The operator map to use + * @return $this Fluent interface + */ + public function setOperator(array $operator) + { + $this->operator = $operator; + return $this; + } + + /** + * Set all implicit values ('after 30 minutes') to be in the past ('after -30 minutes') + * + * @param True $bool True to set all timestring in the past + * @return $this Fluent interface + */ + public function setForcePastValue($bool = true) + { + if ($bool) { + $this->forcedPrefix = '-'; + } else { + $this->forcedPrefix = null; + } + return $this; + } + + /** + * Set all implicit values ('after 30 minutes') to be in the future ('after +30 minutes') + * + * @param True $bool True to set all timestring in the future + * @return $this Fluent interface + */ + public function setForceFutureValue($bool = true) + { + if ($bool) { + $this->forcedPrefix = '+'; + } else { + $this->forcedPrefix = null; + } + return $this; + } +} \ No newline at end of file diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index b38478b38..9af60a85e 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -57,6 +57,7 @@ class Monitoring_ListController extends MonitoringController * @var Backend */ protected $backend; + /** * Compact layout name * @@ -432,7 +433,6 @@ class Monitoring_ListController extends MonitoringController */ private function createTabs() { - $tabs = $this->getTabs(); $tabs->extend(new OutputFormat()) ->extend(new DashboardAction()); diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatusQuery.php index a249c9e5b..c29ccbfe7 100644 --- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatusQuery.php +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatusQuery.php @@ -223,7 +223,6 @@ class StatusQuery extends AbstractQuery protected function joinBaseTables() { - // TODO: Shall we always add hostobject? $this->baseQuery = $this->db->select()->from( array('ho' => $this->prefix . 'objects'), array() diff --git a/modules/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php b/modules/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php new file mode 100644 index 000000000..134b5aa16 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Filter/Backend/IdoQueryConverter.php @@ -0,0 +1,45 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Module\Monitoring\Filter\Backend; + + +use Icinga\Filter\Query\Tree; + +class IdoQueryConverter +{ + + + public function treeToSql(Tree $tree) + { + if ($tree->root = null) { + return ''; + } + } +} \ No newline at end of file diff --git a/modules/monitoring/library/Monitoring/Filter/MonitoringFilter.php b/modules/monitoring/library/Monitoring/Filter/MonitoringFilter.php new file mode 100644 index 000000000..4a5b83921 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Filter/MonitoringFilter.php @@ -0,0 +1,108 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Module\Monitoring\Filter; + +use Icinga\Filter\Domain; +use Icinga\Filter\FilterAttribute; +use Icinga\Filter\Query\Node; +use Icinga\Filter\Type\BooleanFilter; +use Icinga\Filter\Type\TextFilter; +use Icinga\Filter\Type\TimeRangeSpecifier; +use Icinga\Module\Monitoring\Filter\Type\StatusFilter; + +/** + * Factory class to create filter for different monitoring objects + * + */ +class MonitoringFilter +{ + + + private static function getNextCheckFilterType() + { + $type = new TimeRangeSpecifier(); + $type->setOperator( + array( + 'Until' => Node::OPERATOR_LESS_EQ, + 'After' => Node::OPERATOR_GREATER_EQ + ) + )->setForceFutureValue(true); + return $type; + } + + private static function getLastCheckFilterType() + { + $type = new TimeRangeSpecifier(); + $type->setOperator( + array( + 'Older Than' => Node::OPERATOR_LESS_EQ, + 'Is Older Than' => Node::OPERATOR_LESS_EQ, + 'Newer Than' => Node::OPERATOR_GREATER_EQ, + 'Is Newer Than' => Node::OPERATOR_GREATER_EQ, + ) + )->setForcePastValue(true); + return $type; + } + + public static function hostFilter() + { + $domain = new Domain('Host'); + + $domain->registerAttribute( + FilterAttribute::create(new TextFilter()) + ->setHandledAttributes('Name', 'Hostname') + ->setField('host_name') + )->registerAttribute( + FilterAttribute::create(StatusFilter::createForHost()) + ->setHandledAttributes('State', 'Status', 'Current Status') + ->setField('host_state') + )->registerAttribute( + FilterAttribute::create(new BooleanFilter(array( + 'host_is_flapping' => 'Flapping', + 'host_problem' => 'In Problem State', + 'host_notifications_enabled' => 'Sending Notifications', + 'host_active_checks_enabled' => 'Active', + 'host_passive_checks_enabled' => 'Accepting Passive Checks', + 'host_handled' => 'Handled', + 'host_in_downtime' => 'In Downtime', + ))) + )->registerAttribute( + FilterAttribute::create(self::getLastCheckFilterType()) + ->setHandledAttributes('Last Check', 'Check') + ->setField('host_last_check') + )->registerAttribute( + FilterAttribute::create(self::getNextCheckFilterType()) + ->setHandledAttributes('Next Check') + ->setField('host_next_check') + ); + return $domain; + } + +} \ No newline at end of file diff --git a/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php b/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php new file mode 100644 index 000000000..b94e858c1 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php @@ -0,0 +1,321 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Module\Monitoring\Filter\Type; + +use Icinga\Filter\Query\Node; +use Icinga\Filter\Type\FilterType; +use Icinga\Filter\Type\TextFilter; +use Icinga\Filter\Type\TimeRangeSpecifier; + +/** + * Filter type for monitoring states + * + * It's best to use the StatusFilter::createForHost and StatusFilter::createForService + * factory methods as those correctly initialize possible states + * + */ +class StatusFilter extends FilterType +{ + /** + * An array containing a mapping of the textual state representation ('Ok', 'Down', etc.) + * as the keys and the numeric value mapped by this state as the value + * + * @var array + */ + private $baseStates = array(); + + /** + * An array containing all possible textual operator tokens mapped to the + * normalized query operator + * + * @var array + */ + private $operators = array( + 'Is' => Node::OPERATOR_EQUALS, + '=' => Node::OPERATOR_EQUALS, + '!=' => Node::OPERATOR_EQUALS_NOT, + 'Is not' => Node::OPERATOR_EQUALS_NOT + ); + + /** + * The type of this filter ('host' or 'service') + * + * @var string + */ + private $type = ''; + + /** + * The timerange subfilter that can be appended to this filter + * + * @var TimeRangeSpecifier + */ + private $subFilter; + + /** + * Create a new StatusFilter and initialize the internal state correctly. + * + * It's best to use the factory methods instead of new as a call to + * setBaseStates is necessary on direct creation + * + */ + public function __construct() + { + $this->subFilter = new TimeRangeSpecifier(); + } + + /** + * Set the type for this filter (host or service) + * + * @param String $type Either 'host' or 'service' + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * Create a StatusFilter instance that has been initialized for host status filters + * + * @return StatusFilter The ready-to-use host status filter + */ + public static function createForHost() + { + $status = new StatusFilter(); + $status->setBaseStates( + array( + 'Up' => 0, + 'Down' => 1, + 'Unreachable' => 2, + 'Pending' => 99 + ) + ); + $status->setType('host'); + return $status; + } + + /** + * Create a StatusFilter instance that has been initialized for service status filters + * + * @return StatusFilter The ready-to-use service status filter + */ + public static function createForService() + { + $status = new StatusFilter(); + $status->setType(self::TYPE_SERVICE); + $status->setBaseStates( + array( + 'Ok' => 0, + 'Warning' => 1, + 'Critical' => 2, + 'Unknown' => 3, + 'Pending' => 99 + + ) + ); + $status->setType('service'); + return $status; + } + + /** + * Return proposals for the given query part + * + * @param String $query The part of the query that this specifier should parse + * + * @return array An array containing 0..* proposal text tokens + */ + public function getProposalsForQuery($query) + { + if ($query == '') { + return $this->getOperators(); + } + $proposals = array(); + foreach ($this->getOperators() as $operator) { + if (stripos($operator, $query) === 0 && strlen($operator) < strlen($query)) { + $proposals[] = self::markDifference($operator, $query); + } elseif (stripos($query, $operator) === 0) { + $subQuery = trim(substr($query, strlen($operator))); + $proposals = $this->getValueProposalsForQuery($subQuery); + } + } + return $proposals; + } + + /** + * Return an array containing all possible states + * + * @return array An array containing all states mapped by this filter + */ + private function getAllStates() + { + return array_keys($this->baseStates); + } + + /** + * Return possible tokens for completing a partial query that already contains an operator + * + * @param String $query The partial query containing the operator + * + * @return array An array of strings that reflect possible query completions + */ + private function getValueProposalsForQuery($query) + { + if ($query == '') { + return $this->getAllStates(); + } + $proposals = array(); + + foreach ($this->getAllStates() as $state) { + if (self::startsWith($query, $state)) { + $subQuery = trim(substr($query, strlen($state))); + $proposals = array_merge($proposals, $this->subFilter->getProposalsForQuery($subQuery)); + } elseif (self::startsWith($state, $query)) { + $proposals[] = self::markDifference($state, $query); + } + } + return $proposals; + } + + /** + * Return an tuple containing the operator as the first, the value as the second and a possible subquery as the + * third element by parsing the given query + * + * The subquery contains the time information for this status if given + * + * @param String $query The Query to parse with this filter + * + * @return array An array with three elements: array(operator, value, subQuery) or filled with nulls + * if the query is not valid + */ + private function getOperatorValueArray($query) + { + $result = array(null, null, null); + foreach ($this->getOperators() as $operator) { + if (stripos($query, $operator) === 0) { + $result[0] = $operator; + break; + } + } + if ($result[0] === null) { + return $result; + } + $subQuery = trim(substr($query, strlen($result[0]))); + + foreach ($this->getAllStates() as $state) { + if (self::startsWith($subQuery, $state)) { + $result[1] = $state; + } + } + $result[2] = trim(substr($subQuery, strlen($result[1]))); + if ($result[2] && !$this->subFilter->isValidQuery($result[2])) { + return array(null, null, null); + } + return $result; + } + + /** + * Return an array containing the textual presentation of all possible operators + * + * @return array + */ + public function getOperators() + { + return array_keys($this->operators); + } + + /** + * Return true if the given query is a valid, complete query + * + * @param String $query The query to test for being valid and complete + * + * @return bool True when this query is valid, otherwise false + */ + public function isValidQuery($query) + { + $result = $this->getOperatorValueArray($query); + return $result[0] !== null && $result[1] !== null; + } + + /** + * Create a Tree Node from this filter query + * + * @param String $query The query to parse and turn into a Node + * @param String $leftOperand The field to use for the status + * + * @return Node A node object to be added to a query tree + */ + public function createTreeNode($query, $leftOperand) + { + list($operator, $valueSymbol, $timeSpec) = $this->getOperatorValueArray($query); + if ($operator === null || $valueSymbol === null) { + return null; + } + $node = Node::createOperatorNode( + $this->operators[$operator], + $leftOperand, + $this->resolveValue($valueSymbol) + ); + if ($timeSpec) { + $left = $node; + $node = Node::createAndNode(); + $node->left = $left; + + $node->right = $this->subFilter->createTreeNode($timeSpec, $this->type . '_last_state_change'); + $node->right->parent = $node; + $node->left->parent = $node; + } + return $node; + } + + /** + * Return the numeric representation of state given to this filter + * + * @param String $valueSymbol The state string from the query + * + * @return int The numeric state mapped by $valueSymbol or null if it's an invalid state + */ + private function resolveValue($valueSymbol) + { + if (isset($this->baseStates[$valueSymbol])) { + return $this->baseStates[$valueSymbol]; + } + return null; + } + + /** + * Set possible states for this filter + * + * Only required when this filter isn't created by one of it's factory methods + * + * @param array $states The states in an associative statename => numeric representation array + */ + public function setBaseStates(array $states) + { + $this->baseStates = $states; + } +} \ No newline at end of file diff --git a/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php b/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php new file mode 100644 index 000000000..3767928a7 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php @@ -0,0 +1,213 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + + +namespace Icinga\Module\Monitoring\Filter; + + +use Icinga\Filter\Query\Tree; +use Icinga\Filter\Query\Node; +use Icinga\Web\Url; +use Icinga\Application\Logger; + +class UrlViewFilter +{ + const FILTER_TARGET = 'target'; + const FILTER_OPERATOR = 'operator'; + const FILTER_VALUE = 'value'; + const FILTER_ERROR = 'error'; + + private function evaluateNode(Node $node) + { + switch($node->type) { + + case Node::TYPE_OPERATOR: + return urlencode($node->left) . $node->operator . urlencode($node->right); + case Node::TYPE_AND: + return $this->evaluateNode($node->left) . '&' . $this->evaluateNode($node->right); + case Node::TYPE_OR: + return $this->evaluateNode($node->left) . '|' . $this->evaluateNode($node->right); + } + } + + public function fromTree(Tree $filter) + { + return $this->evaluateNode($filter->root); + } + + public function parseUrl($query = "") + { + $query = $query ? $query : $_SERVER['QUERY_STRING']; + $tokens = $this->tokenizeQuery($query); + $tree = new Tree(); + foreach ($tokens as $token) { + if ($token === '&') { + $tree->insert(Node::createAndNode()); + } elseif ($token === '|') { + $tree->insert(Node::createOrNode()); + } elseif (is_array($token)) { + $tree->insert( + Node::createOperatorNode( + $token[self::FILTER_OPERATOR], + $token[self::FILTER_TARGET], + $token[self::FILTER_VALUE] + ) + ); + } + } + return $tree; + + } + + + private function tokenizeQuery($query) + { + $tokens = array(); + $state = self::FILTER_TARGET; + + for ($i = 0;$i <= strlen($query); $i++) { + + switch ($state) { + case self::FILTER_TARGET: + list($i, $state) = $this->parseTarget($query, $i, $tokens); + break; + case self::FILTER_VALUE: + list($i, $state) = $this->parseValue($query, $i, $tokens); + break; + case self::FILTER_ERROR: + list($i, $state) = $this->skip($query, $i, $tokens); + break; + } + } + + return $tokens; + } + + private function getMatchingOperator($query, $i) + { + $operatorToUse = ''; + foreach (Node::$operatorList as $operator) { + if (substr($query, $i, strlen($operator)) === $operator) { + if (strlen($operatorToUse) < strlen($operator)) { + $operatorToUse = $operator; + } + } + } + return $operatorToUse; + } + + private function parseTarget($query, $currentPos, array &$tokenList) + { + $conjunctions = array('&', '|'); + $i = $currentPos; + for ($i; $i < strlen($query); $i++) { + $currentChar = $query[$i]; + // test if operator matches + $operator = $this->getMatchingOperator($query, $i); + + // Test if we're at an operator field right now, then add the current token + // without value to the tokenlist + if($operator !== '') { + $tokenList[] = array( + self::FILTER_TARGET => urldecode(substr($query, $currentPos, $i - $currentPos)), + self::FILTER_OPERATOR => $operator + ); + // -1 because we're currently pointing at the first character of the operator + $newOffset = $i + strlen($operator) - 1; + return array($newOffset, self::FILTER_VALUE); + } + + // Implicit value token (test=1|2) + if (in_array($currentChar, $conjunctions) || $i + 1 == strlen($query)) { + $nrOfSymbols = count($tokenList); + if ($nrOfSymbols <= 2) { + return array($i, self::FILTER_TARGET); + } + + $lastState = &$tokenList[$nrOfSymbols-2]; + + if (is_array($lastState)) { + $tokenList[] = array( + self::FILTER_TARGET => urldecode($lastState[self::FILTER_TARGET]), + self::FILTER_OPERATOR => $lastState[self::FILTER_OPERATOR], + ); + return $this->parseValue($query, $currentPos, $tokenList); + } + return array($i, self::FILTER_TARGET); + } + } + + return array($i, self::FILTER_TARGET); + } + + + private function parseValue($query, $currentPos, array &$tokenList) + { + + $i = $currentPos; + $conjunctions = array('&', '|'); + $nrOfSymbols = count($tokenList); + + if ($nrOfSymbols == 0) { + return array($i, self::FILTER_TARGET); + } + $lastState = &$tokenList[$nrOfSymbols-1]; + for ($i; $i < strlen($query); $i++) { + $currentChar = $query[$i]; + if (in_array($currentChar, $conjunctions)) { + break; + } + } + $length = $i - $currentPos; + // No value given + if ($length === 0) { + array_pop($tokenList); + array_pop($tokenList); + return array($currentPos, self::FILTER_TARGET); + } + $lastState[self::FILTER_VALUE] = urldecode(substr($query, $currentPos, $length)); + + if (in_array($currentChar, $conjunctions)) { + $tokenList[] = $currentChar; + } + return array($i, self::FILTER_TARGET); + } + + private function skip($query, $currentPos, array &$tokenList) + { + $conjunctions = array('&', '|'); + for ($i = $currentPos; strlen($query); $i++) { + $currentChar = $query[$i]; + if (in_array($currentChar, $conjunctions)) { + return array($i, self::FILTER_TARGET); + } + } + } + + +} \ No newline at end of file diff --git a/modules/monitoring/test/php/library/Filter/Type/StatusFilterTest.php b/modules/monitoring/test/php/library/Filter/Type/StatusFilterTest.php new file mode 100644 index 000000000..f76033b44 --- /dev/null +++ b/modules/monitoring/test/php/library/Filter/Type/StatusFilterTest.php @@ -0,0 +1,142 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Test\Modules\Monitoring\Library\Filter\Type; + +use Icinga\Module\Monitoring\Filter\Type\StatusFilter; +use Icinga\Filter\Type\TimeRangeSpecifier; +use Icinga\Filter\Query\Node; +use Icinga\Test\BaseTestCase; + +// @codingStandardsIgnoreStart +require_once realpath(__DIR__ . '/../../../../../../../library/Icinga/Test/BaseTestCase.php'); +require_once realpath(BaseTestCase::$libDir .'/Filter/Query/Node.php'); +require_once realpath(BaseTestCase::$libDir .'/Filter/QueryProposer.php'); +require_once realpath(BaseTestCase::$libDir .'/Filter/Type/FilterType.php'); +require_once realpath(BaseTestCase::$libDir .'/Filter/Type/TimeRangeSpecifier.php'); +require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/Type/StatusFilter.php'); +// @codingStandardsIgnoreEnd + + +class StatusFilterTest extends BaseTestCase +{ + public function testOperatorProposal() + { + $searchType = StatusFilter::createForHost(); + $this->assertEquals( + $searchType->getOperators(), + $searchType->getProposalsForQuery(''), + 'Assert all possible operators to be returned when monitoring status has no further query input' + ); + } + + public function testStateTypeProposal() + { + $searchType = StatusFilter::createForHost(); + $this->assertEquals( + array('{Pen}ding'), + $searchType->getProposalsForQuery('is Pen'), + 'Assert StatusFilter to complete partial queries' + ); + } + + public function testTimeRangeProposal() + { + $subFilter = new TimeRangeSpecifier(); + $searchType = StatusFilter::createForHost(); + $this->assertEquals( + $subFilter->getOperators(), + $searchType->getProposalsForQuery('is Pending'), + 'Assert StatusFilter to chain TimeRangeSpecifier at the end' + ); + + $this->assertEquals( + $subFilter->timeExamples, + $searchType->getProposalsForQuery('is Pending Since'), + 'Assert TimeRange time examples to be proposed' + ); + } + + public function testQueryNodeCreation() + { + $searchType = StatusFilter::createForHost(); + $treeNode = $searchType->createTreeNode('is down', 'host_current_state'); + $this->assertEquals( + 'host_current_state', + $treeNode->left, + 'Assert the left treenode to represent the state field given to the StatusFilter' + ); + $this->assertEquals( + 1, + $treeNode->right, + 'Assert the right treenode to contain the numeric status for "Down"' + ); + $this->assertEquals( + Node::TYPE_OPERATOR, + $treeNode->type, + 'Assert the treenode to be an operator node' + ); + $this->assertEquals( + Node::OPERATOR_EQUALS, + $treeNode->operator, + 'Assert the treenode operator to be "Equals"' + ); + } + + public function testQueryNodeCreationWithTime() + { + $searchType = StatusFilter::createForHost(); + + $treeNode = $searchType->createTreeNode('is down since yesterday', 'host_current_state'); + $this->assertEquals( + Node::TYPE_AND, + $treeNode->type, + 'Assert and and node to be returned when an additional time specifier is appended' + ); + $this->assertEquals( + Node::TYPE_OPERATOR, + $treeNode->left->type, + 'Assert the left node to be the original query (operator)' + ); + $this->assertEquals( + 'host_current_state', + $treeNode->left->left, + 'Assert the left node to be the original query (field)' + ); + $this->assertEquals( + Node::TYPE_OPERATOR, + $treeNode->right->type, + 'Assert the right node to be the time specifier query (operator)' + ); + $this->assertEquals( + 'host_last_state_change', + $treeNode->right->left, + 'Assert the right node to be the time specifier query (field)' + ); + } +} \ No newline at end of file diff --git a/modules/monitoring/test/php/library/Filter/UrlViewFilterTest.php b/modules/monitoring/test/php/library/Filter/UrlViewFilterTest.php new file mode 100644 index 000000000..060d4fa27 --- /dev/null +++ b/modules/monitoring/test/php/library/Filter/UrlViewFilterTest.php @@ -0,0 +1,179 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} +namespace Test\Modules\Monitoring\Library\Filter; + +use Icinga\Module\Monitoring\Filter\Type\StatusFilter; +use Icinga\Filter\Type\TimeRangeSpecifier; +use Icinga\Filter\Query\Node; +use Icinga\Filter\Filter; +use Icinga\Filter\Type\TextFilter; +use Icinga\Filter\FilterAttribute; +use Icinga\Module\Monitoring\Filter\UrlViewFilter; +use Icinga\Protocol\Ldap\Exception; +use Icinga\Test\BaseTestCase; + +// @codingStandardsIgnoreStart +require_once realpath(__DIR__ . '/../../../../../../library/Icinga/Test/BaseTestCase.php'); +require_once realpath(BaseTestCase::$libDir . '/Filter/QueryProposer.php'); +require_once realpath(BaseTestCase::$libDir . '/Filter/Filter.php'); +require_once realpath(BaseTestCase::$libDir . '/Filter/FilterAttribute.php'); +require_once realpath(BaseTestCase::$libDir . '/Filter/Domain.php'); +require_once realpath(BaseTestCase::$libDir . '/Filter/Query/Node.php'); +require_once realpath(BaseTestCase::$libDir . '/Filter/Query/Tree.php'); +require_once realpath(BaseTestCase::$libDir . '/Filter/Type/FilterType.php'); +require_once realpath(BaseTestCase::$libDir . '/Filter/Type/TextFilter.php'); +require_once realpath(BaseTestCase::$libDir .'/Filter/Type/TimeRangeSpecifier.php'); +require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/Type/StatusFilter.php'); +require_once realpath(BaseTestCase::$moduleDir .'/monitoring/library/Monitoring/Filter/UrlViewFilter.php'); + + +class UrlViewFilterTest extends BaseTestCase +{ + public function testUrlParamCreation() + { + $searchEngine = new Filter(); + $searchEngine->createFilterDomain('host') + ->registerAttribute( + FilterAttribute::create(new TextFilter()) + ->setHandledAttributes('attr1') + )->registerAttribute( + FilterAttribute::create(new TextFilter()) + ->setHandledAttributes('attr2') + )->registerAttribute( + FilterAttribute::create(new TextFilter()) + ->setHandledAttributes('attr3') + )->registerAttribute( + FilterAttribute::create(StatusFilter::createForHost()) + ->setHandledAttributes('attr4') + )->registerAttribute( + FilterAttribute::create(StatusFilter::createForHost()) + ->setHandledAttributes('attr5') + ); + $query = 'attr1 is not \'Hans wurst\'' + . ' or attr2 contains something ' + . ' and attr3 starts with bla' + . ' or attr4 is DOWN since "yesterday"' + . ' and attr5 is UP'; + + $tree = $searchEngine->createQueryTreeForFilter($query); + $filterFactory = new UrlViewFilter(); + $uri = $filterFactory->fromTree($tree); + $this->assertEquals( + 'attr1!=Hans+wurst|attr2=%2Asomething%2A&attr3=%2Abla|attr4=1&host_last_state_change>=yesterday&attr5=0', + $uri, + 'Assert a correct query to be returned when parsing a more complex query ("'. $query .'")' + ); + } + + public function testTreeFromSimpleKeyValueUrlCreation() + { + $filterFactory = new UrlViewFilter(); + $tree = $filterFactory->parseUrl('attr1!=Hans+Wurst'); + $this->assertEquals( + $tree->root->type, + Node::TYPE_OPERATOR, + 'Assert one operator node to exist for a simple filter' + ); + $this->assertEquals( + $tree->root->operator, + Node::OPERATOR_EQUALS_NOT, + 'Assert the operator to be !=' + ); + $this->assertEquals( + $tree->root->left, + 'attr1', + 'Assert the field to be set correctly' + ); + $this->assertEquals( + $tree->root->right, + 'Hans Wurst', + 'Assert the value to be set correctly' + ); + } + + public function testConjunctionFilterInUrl() + { + $filterFactory = new UrlViewFilter(); + $query = 'attr1!=Hans+Wurst&test=test123|bla=1'; + $tree = $filterFactory->parseUrl($query); + $this->assertEquals($tree->root->type, Node::TYPE_AND, 'Assert the root of the filter tree to be an AND node'); + $this->assertEquals($filterFactory->fromTree($tree), $query, 'Assert the tree to map back to the query'); + } + + public function testImplicitConjunctionInUrl() + { + $filterFactory = new UrlViewFilter(); + $query = 'attr1!=Hans+Wurst&test=test123|bla=1|2|3'; + $tree = $filterFactory->parseUrl($query); + $this->assertEquals($tree->root->type, Node::TYPE_AND, 'Assert the root of the filter tree to be an AND node'); + $this->assertEquals( + 'attr1!=Hans+Wurst&test=test123|bla=1|bla=2|bla=3', + $filterFactory->fromTree($tree), + 'Assert the tree to map back to the query in an explicit form' + ); + } + + public function testMissingValuesInQueries() + { + $filterFactory = new UrlViewFilter(); + $queryStr = 'attr1!=Hans+Wurst&test='; + $tree = $filterFactory->parseUrl($queryStr); + $query = $filterFactory->fromTree($tree); + $this->assertEquals('attr1!=Hans+Wurst', $query, 'Assert the valid part of a query to be used'); + } + + public function testErrorInQueries() + { + $filterFactory = new UrlViewFilter(); + $queryStr = 'test=&attr1!=Hans+Wurst'; + $tree = $filterFactory->parseUrl($queryStr); + $query = $filterFactory->fromTree($tree); + $this->assertEquals('attr1!=Hans+Wurst', $query, 'Assert the valid part of a query to be used'); + } + + public function testSenselessConjunctions() + { + $filterFactory = new UrlViewFilter(); + $queryStr = 'test=&|/5/|&attr1!=Hans+Wurst'; + $tree = $filterFactory->parseUrl($queryStr); + $query = $filterFactory->fromTree($tree); + $this->assertEquals('attr1!=Hans+Wurst', $query, 'Assert the valid part of a query to be used'); + } + + public function testRandomString() + { + $filter = ''; + $filterFactory = new UrlViewFilter(); + + for ($i=0; $i<10;$i++) { + $filter .= str_shuffle('&|ds& wra =!<>|dsgs=,-G'); + $tree = $filterFactory->parseUrl($filter); + } + + } +} diff --git a/public/js/icinga/components/semanticsearch.js b/public/js/icinga/components/semanticsearch.js new file mode 100644 index 000000000..6593141f4 --- /dev/null +++ b/public/js/icinga/components/semanticsearch.js @@ -0,0 +1,136 @@ +// {{{ICINGA_LICENSE_HEADER}}} +/** + * This file is part of Icinga 2 Web. + * + * Icinga 2 Web - Head for multiple monitoring backends. + * Copyright (C) 2013 Icinga Development Team + * + * 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. + * + * @copyright 2013 Icinga Development Team + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} +/*global Icinga:false, document: false, define:false require:false base_url:false console:false */ + +/** + * Ensures that our date/time controls will work on every browser (natively or javascript based) + */ +define(['jquery', 'logging', 'URIjs/URI'], function($, log, URI) { + 'use strict'; + + return function(inputDOM) { + this.inputDom = $(inputDOM); + this.form = this.inputDom.parents('form').first(); + this.formUrl = URI(this.form.attr('action')); + this.lastTokens = []; + this.lastQueuedEvent = null; + this.pendingRequest = null; + + this.construct = function() { + this.registerControlListener(); + }; + + this.getProposal = function() { + var text = this.inputDom.val().trim(); + + try { + if (this.pendingRequest) { + this.pendingRequest.abort(); + } + this.pendingRequest = $.ajax({ + data: { + 'cache' : (new Date()).getTime(), + 'query' : text + }, + headers: { + 'Accept': 'application/json' + }, + url: this.formUrl + }).done(this.showProposals.bind(this)).fail(function() {}); + } catch(exception) { + console.log(exception); + } + }; + + this.applySelectedProposal = function(token) { + var currentText = $.trim(this.inputDom.val()); + var substr = token.match(/^(\{.*\})/); + if (substr !== null) { + token = token.substr(substr[0].length); + } else { + token = ' ' + token; + } + + currentText += token; + this.inputDom.val(currentText); + this.inputDom.popover('hide'); + this.inputDom.focus(); + }; + + this.showProposals = function(tokens, state, args) { + + var jsonRep = args.responseText; + + + if (tokens.length === 0) { + return this.inputDom.popover('destroy'); + } + this.lastTokens = jsonRep; + + var list = $('